Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/helpers/purchase-operation-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ interface CheckoutStartParams {
// Customer data
customerEmail?: string;
metadata?: PurchaseMetadata;
// Resolved from selectedLocale/defaultLocale at the public API layer.
// Future: consider adding localeSource?: "selected" | "browser".
locale?: string;
}

export interface OperationSessionSuccessfulResult {
Expand Down Expand Up @@ -184,6 +187,7 @@ export class PurchaseOperationHelper {
paywallId,
customerEmail,
metadata,
locale,
}: CheckoutStartParams): Promise<WebBillingCheckoutStartResponse> {
try {
const traceId = this.eventsTracker.getTraceId();
Expand All @@ -200,6 +204,7 @@ export class PurchaseOperationHelper {
paywallId,
customerEmail,
metadata,
locale,
});
this.operationSessionId = checkoutStartResponse.operation_session_id;
return checkoutStartResponse;
Expand Down Expand Up @@ -258,7 +263,10 @@ export class PurchaseOperationHelper {
}
}

async checkoutComplete(email?: string): Promise<CheckoutCompleteResponse> {
async checkoutComplete(
email?: string,
locale?: string,
): Promise<CheckoutCompleteResponse> {
const operationSessionId = this.operationSessionId;
if (!operationSessionId) {
throw new PurchaseFlowError(
Expand All @@ -268,7 +276,11 @@ export class PurchaseOperationHelper {
}

try {
return await this.backend.postCheckoutComplete(operationSessionId, email);
return await this.backend.postCheckoutComplete(
operationSessionId,
email,
locale,
);
} catch (error) {
if (error instanceof PurchasesError) {
throw PurchaseFlowError.fromPurchasesError(
Expand Down
14 changes: 14 additions & 0 deletions src/networking/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ interface CheckoutStartRequestParams {
// Customer data
customerEmail?: string;
metadata?: PurchaseMetadata;
// Locale for lifecycle emails.
locale?: string;
}

export class Backend {
Expand Down Expand Up @@ -189,6 +191,7 @@ export class Backend {
paywallId,
customerEmail,
metadata,
locale,
}: CheckoutStartRequestParams): Promise<T> {
type CheckoutStartRequestBody = {
app_user_id: string;
Expand All @@ -206,6 +209,7 @@ export class Backend {
email?: string;
metadata?: PurchaseMetadata;
trace_id: string;
locale?: string;
paywall?: {
paywall_id: string;
};
Expand Down Expand Up @@ -256,6 +260,10 @@ export class Backend {
};
}

if (locale) {
requestBody.locale = locale;
}

return (await performRequest<CheckoutStartRequestBody, T>(
new CheckoutStartEndpoint(),
{
Expand Down Expand Up @@ -299,15 +307,21 @@ export class Backend {
async postCheckoutComplete(
operationSessionId: string,
email?: string,
locale?: string,
): Promise<CheckoutCompleteResponse> {
type CheckoutCompleteRequestBody = {
email?: string;
locale?: string;
};

const requestBody: CheckoutCompleteRequestBody = {
email: email,
};

if (locale) {
requestBody.locale = locale;
}

return await performRequest<
CheckoutCompleteRequestBody,
CheckoutCompleteResponse
Expand Down
3 changes: 3 additions & 0 deletions src/paddle/paddle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ interface PaddleStartCheckoutParams {
purchaseOption: PurchaseOption;
customerEmail?: string;
metadata?: PurchaseMetadata;
locale?: string;
}

export class PaddleService {
Expand Down Expand Up @@ -127,6 +128,7 @@ export class PaddleService {
purchaseOption,
customerEmail,
metadata,
locale,
}: PaddleStartCheckoutParams): Promise<PaddleCheckoutStartResponse> {
try {
const traceId = this.eventsTracker.getTraceId();
Expand All @@ -139,6 +141,7 @@ export class PaddleService {
traceId,
customerEmail: customerEmail ?? undefined,
metadata,
locale,
});

await this.initializePaddle(
Expand Down
22 changes: 22 additions & 0 deletions src/tests/helpers/purchase-operation-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,28 @@ describe("PurchaseOperationHelper", () => {
});
});

test("checkoutStart passes locale to backend when provided", async () => {
const mockPostCheckoutStart = vi
.spyOn(backend, "postCheckoutStart")
.mockResolvedValue(checkoutStartResponse);

await purchaseOperationHelper.checkoutStart({
appUserId: "test-app-user-id",
productId: "test-product-id",
purchaseOption: { id: "test-option-id", priceId: "test-price-id" },
presentedOfferingContext: {
offeringIdentifier: "test-offering-id",
targetingContext: null,
placementIdentifier: null,
},
locale: "es",
});

expect(mockPostCheckoutStart).toHaveBeenCalledWith(
expect.objectContaining({ locale: "es" }),
);
});

test("prepareCheckout returns the backend response", async () => {
setCheckoutPrepareResponse(
HttpResponse.json(checkoutPrepareResponse, {
Expand Down
77 changes: 77 additions & 0 deletions src/tests/networking/backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,53 @@ describe("postCheckoutStart request", () => {
expect(requestBody.paywall).toBeUndefined();
});

test("includes locale in request when provided", async () => {
setCheckoutStartResponse(
HttpResponse.json(checkoutStartResponse, { status: 200 }),
);

await backend.postCheckoutStart({
appUserId: "someAppUserId",
productId: "monthly",
presentedOfferingContext: {
offeringIdentifier: "offering_1",
targetingContext: null,
placementIdentifier: null,
},
purchaseOption: { id: "base_option", priceId: "test_price_id" },
traceId: "test-trace-id",
locale: "es",
});

expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1);
const request = purchaseMethodAPIMock.mock.calls[0][0].request;
const requestBody = await request.json();
expect(requestBody.locale).toBe("es");
});

test("omits locale from request when not provided", async () => {
setCheckoutStartResponse(
HttpResponse.json(checkoutStartResponse, { status: 200 }),
);

await backend.postCheckoutStart({
appUserId: "someAppUserId",
productId: "monthly",
presentedOfferingContext: {
offeringIdentifier: "offering_1",
targetingContext: null,
placementIdentifier: null,
},
purchaseOption: { id: "base_option", priceId: "test_price_id" },
traceId: "test-trace-id",
});

expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1);
const request = purchaseMethodAPIMock.mock.calls[0][0].request;
const requestBody = await request.json();
expect(requestBody.locale).toBeUndefined();
});

test("throws an error if the backend returns a server error", async () => {
setCheckoutStartResponse(
HttpResponse.json(null, { status: StatusCodes.INTERNAL_SERVER_ERROR }),
Expand Down Expand Up @@ -992,6 +1039,36 @@ describe("postCheckoutComplete request", () => {
expect(result).toEqual(checkoutCompleteResponse);
});

test("includes locale in request when provided", async () => {
setCheckoutCompleteResponse(
HttpResponse.json(checkoutCompleteResponse, { status: 200 }),
);

await backend.postCheckoutComplete(
"someOperationSessionId",
undefined,
"es",
);

expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1);
const request = purchaseMethodAPIMock.mock.calls[0][0].request;
const requestBody = await request.json();
expect(requestBody.locale).toBe("es");
});

test("omits locale from request when not provided", async () => {
setCheckoutCompleteResponse(
HttpResponse.json(checkoutCompleteResponse, { status: 200 }),
);

await backend.postCheckoutComplete("someOperationSessionId");

expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1);
const request = purchaseMethodAPIMock.mock.calls[0][0].request;
const requestBody = await request.json();
expect(requestBody.locale).toBeUndefined();
});

test("throws an error if the backend returns a server error", async () => {
setCheckoutCompleteResponse(
HttpResponse.json(null, { status: StatusCodes.INTERNAL_SERVER_ERROR }),
Expand Down
17 changes: 17 additions & 0 deletions src/tests/paddle/paddle-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,23 @@ describe("PaddleService", () => {
paddleService.startCheckout(startCheckoutArgs),
).rejects.toThrow(expectedError);
});

test("passes locale to backend when provided", async () => {
vi.mocked(initPaddle).mockResolvedValue(mockPaddleInstance);

const mockPostCheckoutStart = vi
.spyOn(backend, "postCheckoutStart")
.mockResolvedValue(paddleCheckoutStartResponse);

await paddleService.startCheckout({
...startCheckoutArgs,
locale: "es",
});

expect(mockPostCheckoutStart).toHaveBeenCalledWith(
expect.objectContaining({ locale: "es" }),
);
});
});

describe("purchase", () => {
Expand Down
1 change: 1 addition & 0 deletions src/tests/ui/paddle-purchases-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe("PaddlePurchasesUI", () => {
purchaseOption: subscriptionOption,
customerEmail: undefined,
metadata: undefined,
locale: "en",
});
});

Expand Down
26 changes: 26 additions & 0 deletions src/tests/ui/stripe-checkout-purchases-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ describe("StripeCheckoutPurchasesUi", () => {
customerEmail: "[email protected]",
metadata: { utm_term: "something" },
workflowPurchaseContext: { stepId: "test-step-123" },
locale: "en",
});
});
});
Expand All @@ -137,6 +138,7 @@ describe("StripeCheckoutPurchasesUi", () => {
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail: "[email protected]",
metadata: { utm_term: "something" },
locale: "en",
});
});
});
Expand All @@ -162,6 +164,7 @@ describe("StripeCheckoutPurchasesUi", () => {
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail: undefined,
metadata: { utm_term: "something" },
locale: "en",
});
});
});
Expand Down Expand Up @@ -208,6 +211,7 @@ describe("StripeCheckoutPurchasesUi", () => {
customerEmail: "[email protected]",
metadata: { utm_term: "something" },
paywallId: "paywall-abc-123",
locale: "en",
});
});
});
Expand All @@ -232,10 +236,32 @@ describe("StripeCheckoutPurchasesUi", () => {
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail: "[email protected]",
metadata: { utm_term: "something" },
locale: "en",
});
});
});

test("passes selectedLocale as locale to checkoutStart", async () => {
const checkoutStartSpy = vi
.spyOn(purchaseOperationHelperMock, "checkoutStart")
.mockResolvedValue(checkoutStartResponseWithoutStripeParams);

render(StripeCheckoutPurchasesUi, {
props: {
...baseProps,
selectedLocale: "es",
},
});

await waitFor(() => {
expect(checkoutStartSpy).toHaveBeenCalledWith(
expect.objectContaining({
locale: "es",
}),
);
});
});

test("shows sandbox banner when stripe billing environment is sandbox", async () => {
vi.spyOn(purchaseOperationHelperMock, "checkoutStart").mockResolvedValue(
createCheckoutStartResponseWithStripeParams("sandbox"),
Expand Down
7 changes: 5 additions & 2 deletions src/ui/express-purchase-button/express-purchase-button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,10 @@

await StripeService.submitElements(elements);

const completeResponse =
await purchaseOperationHelper.checkoutComplete(email);
const completeResponse = await purchaseOperationHelper.checkoutComplete(
email,
translator.selectedLocale,
);
const newClientSecret = completeResponse?.gateway_params?.client_secret;

if (!newClientSecret) {
Expand Down Expand Up @@ -290,6 +292,7 @@
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail,
metadata,
locale: translator.selectedLocale,
});

const managementUrl = checkoutStartResult.management_url;
Expand Down
1 change: 1 addition & 0 deletions src/ui/paddle-purchases-ui.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
purchaseOption,
customerEmail,
metadata,
locale: selectedLocale,
});
isSandbox = startResponse.paddle_billing_params.is_sandbox;
} catch (e) {
Expand Down
6 changes: 4 additions & 2 deletions src/ui/pages/payment-entry-page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,10 @@

// Helper function to complete the checkout
async function completeCheckout(): Promise<void> {
const completeResponse =
await purchaseOperationHelper.checkoutComplete(email);
const completeResponse = await purchaseOperationHelper.checkoutComplete(
email,
$translator.selectedLocale,
);
const newClientSecret = completeResponse?.gateway_params?.client_secret;
if (newClientSecret) clientSecret = newClientSecret;
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/purchases-ui.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
metadata,
workflowPurchaseContext,
paywallId,
locale: selectedLocale,
})
.then((result) => {
lastError = null;
Expand Down
Loading