From f51c6846e2ed98e24c8f5edf7addef45ea8e6f7d Mon Sep 17 00:00:00 2001 From: Farhan Tariq Date: Mon, 24 Aug 2026 15:34:52 +0500 Subject: [PATCH 1/2] fix(demos): pass the plugin explicitly when discovering gateways CheckoutDemo called callPaymentMethods without a plugin, which silently defaulted to payment_request, then created the session with type: "e_commerce". On sandbox.ottu.net that discovery returns cbk-private, which is not enabled for the e-commerce plugin, so the Checkout API rejected the session: 400 ["pg code `cbk-private` is not enabled for `e_commerce` plugin."] Each demo now declares its plugin once and uses it for both the Payment Methods call and the session type, so the two can no longer drift apart. callPaymentMethods takes plugin as a required PaymentPlugin, removing the default that hid this; RecurringDemo and PaymentJourney also send the type they already display in their request panels. Refs #159191 --- .../CheckoutDemo/CheckoutDemoInner.tsx | 8 ++++++- .../PaymentJourney/PaymentJourneyInner.tsx | 10 +++++++-- .../RecurringDemo/RecurringDemoInner.tsx | 16 ++++++++++---- src/components/WalletDemo/WalletDemoInner.tsx | 4 ++-- src/utils/sandbox.ts | 22 ++++++++++++++++--- src/utils/walletDemoConfig.ts | 4 +++- 6 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/components/CheckoutDemo/CheckoutDemoInner.tsx b/src/components/CheckoutDemo/CheckoutDemoInner.tsx index ce5adaab..8c6cbbd8 100644 --- a/src/components/CheckoutDemo/CheckoutDemoInner.tsx +++ b/src/components/CheckoutDemo/CheckoutDemoInner.tsx @@ -5,11 +5,16 @@ import { createSandboxSession, callPaymentMethods, extractPgCodes, + type PaymentPlugin, } from "@site/src/utils/sandbox"; import { createDemoCallbacks } from "@site/src/utils/checkoutSdk"; import CheckoutSDKEmbed from "@site/src/components/CheckoutSDKEmbed"; import styles from "./styles.module.css"; +// Single-sourced: gateway discovery and session creation must name the same +// plugin, or the session gets pg_codes that are not enabled for it (#159191). +const PLUGIN: PaymentPlugin = "e_commerce"; + type State = | { status: "idle" } | { status: "fetching_methods" } @@ -86,6 +91,7 @@ export default function CheckoutDemoInner() { try { const methodsResponse = await callPaymentMethods({ currencies: ["KWD"], + plugin: PLUGIN, type: "sandbox", tags: ["demo"], }); @@ -94,7 +100,7 @@ export default function CheckoutDemoInner() { const { session_id } = await createSandboxSession({ pg_codes: pgCodes.length > 0 ? pgCodes : ["ottu_sdk"], - type: "e_commerce", + type: PLUGIN, }); dispatch({ type: "SESSION_CREATED", sessionId: session_id }); } catch (err: any) { diff --git a/src/components/PaymentJourney/PaymentJourneyInner.tsx b/src/components/PaymentJourney/PaymentJourneyInner.tsx index 8e278c86..aa417233 100644 --- a/src/components/PaymentJourney/PaymentJourneyInner.tsx +++ b/src/components/PaymentJourney/PaymentJourneyInner.tsx @@ -6,6 +6,7 @@ import { callPaymentMethods, callPaymentStatusQuery, getWebhookBaseUrl, + type PaymentPlugin, } from "@site/src/utils/sandbox"; import { createDemoCallbacks } from "@site/src/utils/checkoutSdk"; import ApiPanel from "@site/src/components/ApiPanel"; @@ -15,6 +16,10 @@ import { TEST_CARD } from "@site/src/components/TestCardCallout"; import { COUNTRIES, DEFAULT_COUNTRY_INDEX } from "./countries"; import styles from "./styles.module.css"; +// Single-sourced: gateway discovery and session creation must name the same +// plugin, or the session gets pg_codes that are not enabled for it (#159191). +const PLUGIN: PaymentPlugin = "payment_request"; + // ── Types ────────────────────────────────────────────── type Status = @@ -325,7 +330,7 @@ export default function PaymentJourneyInner() { const runStep1 = useCallback(async () => { dispatch({ type: "COUNTRY_CONFIRMED" }); try { - const response = await callPaymentMethods({ currencies: [state.selectedCurrency], plugin: "payment_request", operation: "purchase", type: "sandbox", tags: ["demo"] }); + const response = await callPaymentMethods({ currencies: [state.selectedCurrency], plugin: PLUGIN, operation: "purchase", type: "sandbox", tags: ["demo"] }); const pgCodes = response?.payment_methods?.map((m: any) => m.code) ?? response?.pg_codes ?? []; dispatch({ type: "STEP1_DONE", pgCodes, response }); } catch (err: any) { @@ -341,6 +346,7 @@ export default function PaymentJourneyInner() { const webhookUrl = `${getWebhookBaseUrl()}/webhook/${state.orderId}`; const response = await createSandboxSession({ pg_codes: state.pgCodes.length > 0 ? state.pgCodes : ["direct-payment"], + type: PLUGIN, currency_code: state.selectedCurrency, customer_id: "sandbox", extra: { @@ -514,7 +520,7 @@ export default function PaymentJourneyInner() { {(state.status === "step1_done" || isStepExpanded(2)) && ( <> { const webhookUrl = `${getWebhookBaseUrl()}/webhook/${state.orderId}`; const mitRequest = { - type: "e_commerce", + type: PLUGIN, pg_codes: [state.citPgCode], amount: "15", currency_code: "KWD", @@ -387,6 +393,7 @@ export default function RecurringDemoInner() { try { const result = await createSandboxSession({ pg_codes: [state.citPgCode || state.pgCodes[0]], + type: PLUGIN, amount: "15", customer_id: state.customerId, extra: { @@ -411,7 +418,7 @@ export default function RecurringDemoInner() { const startMITTwoStep = useCallback(async () => { const webhookUrl = `${getWebhookBaseUrl()}/webhook/${state.orderId}`; const mitRequest = { - type: "e_commerce", + type: PLUGIN, pg_codes: [state.citPgCode], amount: "15", currency_code: "KWD", @@ -427,6 +434,7 @@ export default function RecurringDemoInner() { try { const session = await createSandboxSession({ pg_codes: [state.citPgCode || state.pgCodes[0]], + type: PLUGIN, amount: "15", customer_id: state.customerId, extra: { diff --git a/src/components/WalletDemo/WalletDemoInner.tsx b/src/components/WalletDemo/WalletDemoInner.tsx index 89201422..c0b03149 100644 --- a/src/components/WalletDemo/WalletDemoInner.tsx +++ b/src/components/WalletDemo/WalletDemoInner.tsx @@ -99,7 +99,7 @@ export default function WalletDemoInner() { try { // Step 1 — Fetch wallet-capable gateways - const filter = WALLET_DEMO.pgFilter || ({} as any); + const filter = WALLET_DEMO.pgFilter; const methodsResponse = await callPaymentMethods({ currencies: [WALLET_DEMO.currency], plugin: filter.plugin, @@ -131,7 +131,7 @@ export default function WalletDemoInner() { // Step 3 — Create Checkout session const { session_id } = await createSandboxSession({ pg_codes: [pgCode], - type: "e_commerce", + type: filter.plugin, amount: WALLET_DEMO.sessionAmount, currency_code: WALLET_DEMO.currency, customer_id: customerId, diff --git a/src/utils/sandbox.ts b/src/utils/sandbox.ts index 19fe0844..6039f294 100644 --- a/src/utils/sandbox.ts +++ b/src/utils/sandbox.ts @@ -38,12 +38,22 @@ export const KSA: ConnectEnv = { // WalletDemo seeds and charges in USD (see walletDemoConfig.ts). export const ACTIVE_CONNECT: ConnectEnv = SANDBOX; +/** + * The Ottu plugin a payment belongs to. + * + * Same vocabulary on both sides of the flow: it is the `plugin` filter on the + * Payment Methods API and the `type` of the Checkout API session. A gateway + * enabled for one plugin is not necessarily enabled for the other, so the two + * calls in a single demo must always use the same value. + */ +export type PaymentPlugin = "e_commerce" | "payment_request"; + export interface CreateSessionOptions { pg_codes: string[]; amount?: string; currency_code?: string; customer_id?: string; - type?: string; + type?: PaymentPlugin; /** Arbitrary extra fields merged into the request body (e.g., payment_type, agreement, payment_instrument, webhook_url) */ extra?: Record; } @@ -151,7 +161,13 @@ export async function callAutoDebit( */ export async function callPaymentMethods(options: { currencies: string[]; - plugin?: string; + /** + * Required: gateways are enabled per plugin, so discovery must state which + * plugin the caller is about to create a session for. Defaulting this is what + * let CheckoutDemo discover `payment_request` gateways and then ask for an + * `e_commerce` session (#159191). + */ + plugin: PaymentPlugin; operation?: string; type?: string; tags?: string[]; @@ -160,7 +176,7 @@ export async function callPaymentMethods(options: { payment_services?: string[]; }): Promise { const body: Record = { - plugin: options.plugin ?? "payment_request", + plugin: options.plugin, operation: options.operation ?? "purchase", currencies: options.currencies, }; diff --git a/src/utils/walletDemoConfig.ts b/src/utils/walletDemoConfig.ts index f28524aa..53011b8c 100644 --- a/src/utils/walletDemoConfig.ts +++ b/src/utils/walletDemoConfig.ts @@ -6,13 +6,15 @@ * The merchant host / Api-Key / SDK key all come from `ACTIVE_CONNECT` * in `./sandbox` — flip that one global to retarget every demo on the site. */ +import type { PaymentPlugin } from "./sandbox"; + export const WALLET_DEMO = { // Wallet on sandbox.ottu.net is enabled for USD only (PG `ottu-sandbox-usd`). currency: "USD", seedAmount: "10.00", sessionAmount: "8.00", pgFilter: { - plugin: "e_commerce", + plugin: "e_commerce" as PaymentPlugin, type: "sandbox", tags: ["demo"], payment_services: ["wallet"], From c322df9d9c334916c2d36cd9afaf8e90c146e33a Mon Sep 17 00:00:00 2001 From: Farhan Tariq Date: Mon, 24 Aug 2026 16:16:35 +0500 Subject: [PATCH 2/2] fix(demos): single-source the session type too Addresses PR review on #174. PaymentJourney's Step 3 request panel still hardcoded type: "payment_request" while the real call used PLUGIN. Harmless today because they happen to match, but it is the exact drift this branch removes, so the panel now reads PLUGIN as well. CreateSessionOptions.type is now required, dropping its ?? "payment_request" fallback. That silent default is the same footgun that caused #159191 on the discovery side; every call site already passes type explicitly, so this is a type-level change only. Refs #159191 --- src/components/PaymentJourney/PaymentJourneyInner.tsx | 2 +- src/utils/sandbox.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/PaymentJourney/PaymentJourneyInner.tsx b/src/components/PaymentJourney/PaymentJourneyInner.tsx index aa417233..2736298a 100644 --- a/src/components/PaymentJourney/PaymentJourneyInner.tsx +++ b/src/components/PaymentJourney/PaymentJourneyInner.tsx @@ -551,7 +551,7 @@ export default function PaymentJourneyInner() { ) : (state.status === "step2_done" || isStepExpanded(3)) && state.sessionId ? ( <> ; } @@ -89,7 +94,7 @@ export async function createSandboxSession( options: CreateSessionOptions ): Promise { const body = { - type: options.type ?? "payment_request", + type: options.type, pg_codes: options.pg_codes, amount: options.amount ?? "20", currency_code: options.currency_code ?? "KWD",