Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 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
8 changes: 8 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
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
47 changes: 47 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
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
5 changes: 5 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: "test@example.com",
metadata: { utm_term: "something" },
workflowPurchaseContext: { stepId: "test-step-123" },
locale: "en",
});
});
});
Expand All @@ -137,6 +138,7 @@ describe("StripeCheckoutPurchasesUi", () => {
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail: "test@example.com",
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: "test@example.com",
metadata: { utm_term: "something" },
paywallId: "paywall-abc-123",
locale: "en",
});
});
});
Expand All @@ -232,6 +236,7 @@ describe("StripeCheckoutPurchasesUi", () => {
rcPackage.webBillingProduct.presentedOfferingContext,
customerEmail: "test@example.com",
metadata: { utm_term: "something" },
locale: "en",
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,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
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
1 change: 1 addition & 0 deletions src/ui/stripe-checkout-purchases-ui.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
metadata,
workflowPurchaseContext,
paywallId,
locale: selectedLocale,
});

if (!result.stripe_billing_params) {
Expand Down