@funnelfox/payments-sdk 0.1.2-beta.9 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -62,7 +62,8 @@ function buildStripeCheckoutSessionBody(params) {
62
62
  }
63
63
  },
64
64
  ...params.upgrade && { upgrade: params.upgrade },
65
- ...params.walletFirst && { wallet_first: params.walletFirst }
65
+ ...params.walletFirst && { wallet_first: params.walletFirst },
66
+ ...params.uiMode && { ui_mode: params.uiMode }
66
67
  };
67
68
  }
68
69
 
@@ -125,55 +126,195 @@ var StripeCheckoutService = class extends HttpService {
125
126
  }
126
127
  };
127
128
 
129
+ // src/providers/stripe/ui/embedded-page.ts
130
+ var StripeEmbeddedPageUi = class {
131
+ checkout = null;
132
+ async mount(stripe, container, host) {
133
+ try {
134
+ this.checkout = await stripe.createEmbeddedCheckoutPage({
135
+ fetchClientSecret: () => host.clientSecret(),
136
+ onComplete: () => host.succeeded()
137
+ });
138
+ } catch (err) {
139
+ host.renderFailed(err, { message: err instanceof Error ? err.message : void 0 });
140
+ if (err instanceof CheckoutError) throw err;
141
+ const detail = err instanceof Error ? err.message : String(err);
142
+ throw new CheckoutError(`Stripe checkout failed: ${detail}`);
143
+ }
144
+ this.checkout.mount(container);
145
+ }
146
+ destroy() {
147
+ this.checkout?.destroy();
148
+ this.checkout = null;
149
+ }
150
+ };
151
+
152
+ // src/providers/stripe/ui/form.ts
153
+ var CHECKOUT_FORM_BETA = "custom_checkout_payment_form_1";
154
+ var StripeFormUi = class {
155
+ constructor(options = {}) {
156
+ this.options = options;
157
+ }
158
+ options;
159
+ loadOptions = { betas: [CHECKOUT_FORM_BETA] };
160
+ checkout = null;
161
+ form = null;
162
+ async mount(stripe, container, host) {
163
+ const { appearance, fonts, loader, layout, expressCheckout } = this.options;
164
+ const startedAt = Date.now();
165
+ try {
166
+ if (typeof stripe.initCheckoutFormSdk !== "function") {
167
+ throw new CheckoutError("Stripe has not granted this account access to Checkout Form");
168
+ }
169
+ this.checkout = stripe.initCheckoutFormSdk({
170
+ clientSecret: host.clientSecret(),
171
+ ...appearance && { appearance },
172
+ ...fonts?.length && { fonts },
173
+ ...loader && { loader }
174
+ });
175
+ this.checkout.on("change", (session) => host.changed(fromSession(session)));
176
+ this.form = this.checkout.createForm({
177
+ ...layout && { layout },
178
+ ...expressCheckout && { expressCheckout }
179
+ });
180
+ this.form.on("ready", () => host.ready(Date.now() - startedAt));
181
+ this.form.on(
182
+ "loaderror",
183
+ (event) => host.renderFailed(event.error, { message: event.error.message })
184
+ );
185
+ this.form.on("change", (event) => host.changed(fromForm(event)));
186
+ this.form.on("cancel", () => host.cancelled());
187
+ this.form.mount(container);
188
+ const actions = await this.loadActions();
189
+ this.form.on("confirm", (event) => void this.confirm(actions, event, host));
190
+ } catch (err) {
191
+ host.renderFailed(err, { message: err instanceof Error ? err.message : void 0 });
192
+ if (err instanceof CheckoutError) throw err;
193
+ const detail = err instanceof Error ? err.message : String(err);
194
+ throw new CheckoutError(`Stripe checkout failed: ${detail}`);
195
+ }
196
+ }
197
+ destroy() {
198
+ this.form?.destroy();
199
+ this.form = null;
200
+ this.checkout = null;
201
+ }
202
+ async loadActions() {
203
+ const result = await this.checkout.loadActions();
204
+ if (result.type !== "success") {
205
+ throw new CheckoutError(`Stripe checkout failed: ${result.error.message}`);
206
+ }
207
+ return result.actions;
208
+ }
209
+ async confirm(actions, event, host) {
210
+ const express = event.source === "checkout-form-ece";
211
+ const paymentMethodType = express ? void 0 : event.paymentMethodType ?? void 0;
212
+ host.confirmed({ paymentMethodType, express });
213
+ try {
214
+ const result = await actions.confirm({ formConfirmEvent: event, redirect: "if_required" });
215
+ if (result.type === "error") {
216
+ host.paymentFailed(result.error, {
217
+ declineCode: result.error.code === "paymentFailed" ? result.error.paymentFailed.declineCode : null,
218
+ message: result.error.message
219
+ });
220
+ return;
221
+ }
222
+ host.succeeded(fromSession(result.session));
223
+ } catch (err) {
224
+ host.paymentFailed(err, { message: err instanceof Error ? err.message : void 0 });
225
+ }
226
+ }
227
+ };
228
+ function fromSession(session) {
229
+ return {
230
+ email: session.email ?? void 0,
231
+ currency: session.currency,
232
+ amountTotal: session.total?.total?.minorUnitsAmount
233
+ };
234
+ }
235
+ function fromForm(event) {
236
+ const { payment, customerDetails, adaptivePricing } = event.value;
237
+ return {
238
+ paymentMethodType: payment?.type,
239
+ email: customerDetails?.email || void 0,
240
+ currency: adaptivePricing?.currency,
241
+ complete: event.complete
242
+ };
243
+ }
244
+
245
+ // src/providers/stripe/ui/index.ts
246
+ function createStripeCheckoutUi(config) {
247
+ switch (config.uiMode) {
248
+ case "form":
249
+ return new StripeFormUi(config.form);
250
+ default:
251
+ return new StripeEmbeddedPageUi();
252
+ }
253
+ }
254
+
128
255
  // src/providers/stripe/index.ts
129
256
  var StripeProvider = class {
130
257
  constructor(config, callbacks, baseUrl, headers) {
131
258
  this.config = config;
132
259
  this.callbacks = callbacks;
133
260
  this.service = new StripeCheckoutService(baseUrl, headers);
261
+ this.ui = createStripeCheckoutUi(config);
134
262
  }
135
263
  config;
136
264
  callbacks;
137
- stripe = null;
138
- embeddedCheckout = null;
139
265
  service;
266
+ ui;
267
+ stripe = null;
268
+ mounted = false;
140
269
  clientSecret = null;
270
+ snapshot = {};
141
271
  async init() {
142
- const stripe = await (0, import_stripe_js.loadStripe)(this.config.publishableKey);
272
+ const stripe = await (0, import_stripe_js.loadStripe)(this.config.publishableKey, this.ui.loadOptions);
143
273
  if (!stripe) {
144
- throw new CheckoutError("Failed to load Stripe.js \u2014 verify your publishableKey");
274
+ const error = new CheckoutError("Failed to load Stripe.js \u2014 verify your publishableKey");
275
+ this.emitError("init", error, { message: error.message });
276
+ throw error;
145
277
  }
146
278
  this.stripe = stripe;
147
279
  }
148
- async render(container, options) {
280
+ async render(container, _options) {
149
281
  if (!this.stripe) {
150
282
  throw new CheckoutError("Provider not initialized \u2014 call init() before render()");
151
283
  }
152
- if (this.embeddedCheckout) {
284
+ if (this.mounted) {
153
285
  throw new CheckoutError("Already rendered \u2014 call destroy() before rendering again");
154
286
  }
155
- try {
156
- this.embeddedCheckout = await this.stripe.createEmbeddedCheckoutPage({
157
- fetchClientSecret: () => this.fetchClientSecret(),
158
- onComplete: () => this.handleComplete(),
159
- onAnalyticsEvent: (event) => console.log("[stripe analytics]", event.eventType, event)
160
- });
161
- } catch (err) {
162
- this.callbacks.onError?.({ provider: "stripe", error: err });
163
- if (err instanceof CheckoutError) throw err;
164
- const detail = err instanceof Error ? err.message : String(err);
165
- throw new CheckoutError(`Stripe checkout failed: ${detail}`);
166
- }
167
- this.embeddedCheckout.mount(container);
287
+ this.mounted = true;
288
+ await this.ui.mount(this.stripe, container, this.host());
168
289
  }
169
290
  async reset() {
170
291
  this.destroy();
171
292
  }
172
293
  destroy() {
173
- this.embeddedCheckout?.destroy();
174
- this.embeddedCheckout = null;
294
+ this.ui.destroy();
295
+ this.mounted = false;
296
+ this.snapshot = {};
175
297
  }
176
- async fetchClientSecret() {
298
+ host() {
299
+ return {
300
+ clientSecret: () => this.createSession(),
301
+ ready: (durationMs) => this.callbacks.onReady?.({ provider: "stripe", durationMs }),
302
+ changed: (patch) => this.merge(patch),
303
+ confirmed: ({ paymentMethodType, express }) => this.callbacks.onConfirm?.({
304
+ provider: "stripe",
305
+ paymentMethodType: paymentMethodType ?? this.snapshot.paymentMethodType,
306
+ express
307
+ }),
308
+ cancelled: () => this.callbacks.onCancel?.({
309
+ provider: "stripe",
310
+ paymentMethodType: this.snapshot.paymentMethodType
311
+ }),
312
+ renderFailed: (error, detail) => this.emitError("render", error, detail),
313
+ paymentFailed: (error, detail) => this.emitError("payment", error, detail),
314
+ succeeded: (outcome) => void this.emitComplete(outcome)
315
+ };
316
+ }
317
+ async createSession() {
177
318
  const secret = await this.service.createCheckoutSession({
178
319
  ...this.config,
179
320
  returnUrl: this.config.returnUrl ?? window.location.href
@@ -182,33 +323,74 @@ var StripeProvider = class {
182
323
  return secret;
183
324
  }
184
325
  // Format: cs_test_{sessionId}_secret_{random}
185
- parseSessionId(clientSecret) {
186
- if (!clientSecret) return null;
187
- const idx = clientSecret.indexOf("_secret_");
188
- return idx !== -1 ? clientSecret.slice(0, idx) : null;
189
- }
190
- async handleComplete() {
191
- const sessionId = this.parseSessionId(this.clientSecret);
192
- if (!sessionId) {
193
- this.callbacks.onComplete?.({ provider: "stripe" });
194
- return;
326
+ sessionId() {
327
+ if (!this.clientSecret) {
328
+ return null;
195
329
  }
196
- try {
197
- const session = await this.service.getSession(sessionId, this.config.accountId);
198
- this.callbacks.onComplete?.({
199
- provider: "stripe",
200
- customerId: session.customer_id ?? void 0,
201
- subscriptionId: session.subscription_id ?? void 0,
202
- invoiceId: session.invoice_id ?? void 0,
203
- amountPaid: session.amount_total ?? void 0,
204
- currency: session.currency ?? void 0,
205
- email: session.email ?? void 0
206
- });
207
- } catch {
208
- this.callbacks.onComplete?.({ provider: "stripe" });
330
+ const idx = this.clientSecret.indexOf("_secret_");
331
+ return idx !== -1 ? this.clientSecret.slice(0, idx) : null;
332
+ }
333
+ merge(patch) {
334
+ const next = { ...this.snapshot };
335
+ for (const [key, value] of Object.entries(patch)) {
336
+ if (value !== void 0) {
337
+ Object.assign(next, { [key]: value });
338
+ }
339
+ }
340
+ this.snapshot = next;
341
+ this.callbacks.onChange?.({ provider: "stripe", ...next });
342
+ }
343
+ /**
344
+ * Reads the session back for the ids only the backend can resolve, falling
345
+ * back to what the client already knows so a paid customer is never
346
+ * reported as a failure.
347
+ */
348
+ async emitComplete(outcome) {
349
+ const sessionId = this.sessionId();
350
+ const fallback = {
351
+ amount_total: outcome?.amountTotal ?? null,
352
+ currency: outcome?.currency ?? null,
353
+ email: outcome?.email ?? null
354
+ };
355
+ let session = {};
356
+ if (sessionId) {
357
+ try {
358
+ session = definedOnly(await this.service.getSession(sessionId, this.config.accountId));
359
+ } catch {
360
+ session = {};
361
+ }
209
362
  }
363
+ this.callbacks.onComplete?.({
364
+ provider: "stripe",
365
+ paymentMethodType: this.snapshot.paymentMethodType,
366
+ ...toCompleteFields({ ...fallback, ...session })
367
+ });
368
+ }
369
+ emitError(phase, error, detail) {
370
+ this.callbacks.onError?.({
371
+ provider: "stripe",
372
+ phase,
373
+ paymentMethodType: this.snapshot.paymentMethodType,
374
+ error,
375
+ ...detail
376
+ });
210
377
  }
211
378
  };
379
+ function definedOnly(session) {
380
+ return Object.fromEntries(
381
+ Object.entries(session).filter(([, value]) => value !== null && value !== void 0)
382
+ );
383
+ }
384
+ function toCompleteFields(session) {
385
+ return {
386
+ customerId: session.customer_id ?? void 0,
387
+ subscriptionId: session.subscription_id ?? void 0,
388
+ invoiceId: session.invoice_id ?? void 0,
389
+ amountPaid: session.amount_total ?? void 0,
390
+ currency: session.currency ?? void 0,
391
+ email: session.email ?? void 0
392
+ };
393
+ }
212
394
 
213
395
  // src/providers/fastspring/index.ts
214
396
  var FastSpringProvider = class {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,27 @@
1
- type PaymentMethodType = 'card' | 'google_pay' | 'apple_pay';
1
+ import { Appearance, CssFontSource, CustomFontSource, StripeCheckoutFormOptions } from '@stripe/stripe-js';
2
2
 
3
+ /**
4
+ * Known values are suggested; providers may report their own ids
5
+ * (Stripe form reports e.g. `sepa_debit`, `link`, `klarna`).
6
+ */
7
+ type PaymentMethodType = 'card' | 'google_pay' | 'apple_pay' | (string & {});
8
+
9
+ /**
10
+ * How Stripe renders checkout. Independent of `managed`, which decides
11
+ * merchant of record — Stripe treats them as two parameters of one session.
12
+ */
13
+ type StripeUiMode = 'embedded_page' | 'form';
14
+ /** Client-side UI options for `uiMode: 'form'`. */
15
+ interface StripeFormOptions {
16
+ /** Form supports the whole Appearance API except `rules`. */
17
+ appearance?: Omit<Appearance, 'rules'>;
18
+ /** Font sources so `appearance.variables.fontFamily` resolves inside the iframe. */
19
+ fonts?: Array<CssFontSource | CustomFontSource>;
20
+ loader?: 'auto' | 'always' | 'never';
21
+ /** Omit to let Stripe pick the layout it expects to convert best. */
22
+ layout?: StripeCheckoutFormOptions['layout'];
23
+ expressCheckout?: StripeCheckoutFormOptions['expressCheckout'];
24
+ }
3
25
  interface StripeCustomer {
4
26
  email?: string;
5
27
  phone?: string;
@@ -41,6 +63,10 @@ interface StripeConfig {
41
63
  upgrade?: boolean;
42
64
  /** When true, surfaces Apple Pay / Google Pay as the primary payment option (Wallet-First mode) */
43
65
  walletFirst?: boolean;
66
+ /** Which checkout UI Stripe renders. Defaults to `embedded_page`. */
67
+ uiMode?: StripeUiMode;
68
+ /** UI options applied only when `uiMode` is `form`. */
69
+ form?: StripeFormOptions;
44
70
  }
45
71
 
46
72
  interface FastSpringConfig {
@@ -79,20 +105,54 @@ interface CheckoutCompleteEvent {
79
105
  currency?: string;
80
106
  email?: string;
81
107
  }
108
+ /**
109
+ * Where a checkout failed. `init`/`render` mean the UI never became usable;
110
+ * `payment` means the buyer's charge was declined or errored.
111
+ */
112
+ type CheckoutErrorPhase = 'init' | 'render' | 'payment';
82
113
  interface CheckoutErrorEvent {
83
114
  provider: ProviderName;
84
115
  paymentMethodType?: PaymentMethodType;
116
+ phase: CheckoutErrorPhase;
117
+ /** Provider decline code when it exposes one, for mapping to a failure reason. */
118
+ declineCode?: string | null;
119
+ message?: string;
85
120
  error: unknown;
86
121
  }
87
122
  interface CheckoutCancelEvent {
88
123
  provider: ProviderName;
89
124
  paymentMethodType?: PaymentMethodType;
90
125
  }
126
+ interface CheckoutReadyEvent {
127
+ provider: ProviderName;
128
+ /** Milliseconds from render() to the checkout UI being usable. */
129
+ durationMs: number;
130
+ }
131
+ /** The buyer authorized the payment; the charge is not confirmed yet. */
132
+ interface CheckoutConfirmEvent {
133
+ provider: ProviderName;
134
+ paymentMethodType?: PaymentMethodType;
135
+ /** Authorized through a wallet button rather than the pay button. */
136
+ express: boolean;
137
+ }
138
+ /** Latest snapshot of what the buyer selected and what they will be charged. */
139
+ interface CheckoutChangeEvent {
140
+ provider: ProviderName;
141
+ paymentMethodType?: PaymentMethodType;
142
+ email?: string;
143
+ /** Currency actually charged — Adaptive Pricing may differ from the listed one. */
144
+ currency?: string;
145
+ amountTotal?: number;
146
+ complete?: boolean;
147
+ }
91
148
 
92
149
  interface CheckoutCallbacks {
93
150
  onComplete?: (event: CheckoutCompleteEvent) => void;
94
151
  onError?: (event: CheckoutErrorEvent) => void;
95
152
  onCancel?: (event: CheckoutCancelEvent) => void;
153
+ onReady?: (event: CheckoutReadyEvent) => void;
154
+ onConfirm?: (event: CheckoutConfirmEvent) => void;
155
+ onChange?: (event: CheckoutChangeEvent) => void;
96
156
  }
97
157
 
98
158
  interface RenderOptions {
@@ -123,9 +183,9 @@ declare class CheckoutError extends Error {
123
183
  constructor(message: string);
124
184
  }
125
185
 
126
- type CreateCheckoutSessionParams = Omit<StripeConfig, 'publishableKey' | 'returnUrl'> & {
186
+ type CreateCheckoutSessionParams = Omit<StripeConfig, 'publishableKey' | 'returnUrl' | 'form'> & {
127
187
  returnUrl: string;
128
188
  };
129
189
  declare function buildStripeCheckoutSessionBody(params: CreateCheckoutSessionParams): Record<string, unknown>;
130
190
 
131
- export { type CheckoutCallbacks, type CheckoutCancelEvent, type CheckoutCompleteEvent, CheckoutError, type CheckoutErrorEvent, type CheckoutInstance, type CreateCheckoutSessionParams, type FastSpringConfig, type PaymentMethodType, type PaymentProvider, type ProviderConfigMap, type ProviderName, type RenderOptions, type StripeCheckoutTrial, type StripeConfig, buildStripeCheckoutSessionBody, initCheckout };
191
+ export { type CheckoutCallbacks, type CheckoutCancelEvent, type CheckoutChangeEvent, type CheckoutCompleteEvent, type CheckoutConfirmEvent, CheckoutError, type CheckoutErrorEvent, type CheckoutErrorPhase, type CheckoutInstance, type CheckoutReadyEvent, type CreateCheckoutSessionParams, type FastSpringConfig, type PaymentMethodType, type PaymentProvider, type ProviderConfigMap, type ProviderName, type RenderOptions, type StripeCheckoutTrial, type StripeConfig, type StripeFormOptions, type StripeUiMode, buildStripeCheckoutSessionBody, initCheckout };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,27 @@
1
- type PaymentMethodType = 'card' | 'google_pay' | 'apple_pay';
1
+ import { Appearance, CssFontSource, CustomFontSource, StripeCheckoutFormOptions } from '@stripe/stripe-js';
2
2
 
3
+ /**
4
+ * Known values are suggested; providers may report their own ids
5
+ * (Stripe form reports e.g. `sepa_debit`, `link`, `klarna`).
6
+ */
7
+ type PaymentMethodType = 'card' | 'google_pay' | 'apple_pay' | (string & {});
8
+
9
+ /**
10
+ * How Stripe renders checkout. Independent of `managed`, which decides
11
+ * merchant of record — Stripe treats them as two parameters of one session.
12
+ */
13
+ type StripeUiMode = 'embedded_page' | 'form';
14
+ /** Client-side UI options for `uiMode: 'form'`. */
15
+ interface StripeFormOptions {
16
+ /** Form supports the whole Appearance API except `rules`. */
17
+ appearance?: Omit<Appearance, 'rules'>;
18
+ /** Font sources so `appearance.variables.fontFamily` resolves inside the iframe. */
19
+ fonts?: Array<CssFontSource | CustomFontSource>;
20
+ loader?: 'auto' | 'always' | 'never';
21
+ /** Omit to let Stripe pick the layout it expects to convert best. */
22
+ layout?: StripeCheckoutFormOptions['layout'];
23
+ expressCheckout?: StripeCheckoutFormOptions['expressCheckout'];
24
+ }
3
25
  interface StripeCustomer {
4
26
  email?: string;
5
27
  phone?: string;
@@ -41,6 +63,10 @@ interface StripeConfig {
41
63
  upgrade?: boolean;
42
64
  /** When true, surfaces Apple Pay / Google Pay as the primary payment option (Wallet-First mode) */
43
65
  walletFirst?: boolean;
66
+ /** Which checkout UI Stripe renders. Defaults to `embedded_page`. */
67
+ uiMode?: StripeUiMode;
68
+ /** UI options applied only when `uiMode` is `form`. */
69
+ form?: StripeFormOptions;
44
70
  }
45
71
 
46
72
  interface FastSpringConfig {
@@ -79,20 +105,54 @@ interface CheckoutCompleteEvent {
79
105
  currency?: string;
80
106
  email?: string;
81
107
  }
108
+ /**
109
+ * Where a checkout failed. `init`/`render` mean the UI never became usable;
110
+ * `payment` means the buyer's charge was declined or errored.
111
+ */
112
+ type CheckoutErrorPhase = 'init' | 'render' | 'payment';
82
113
  interface CheckoutErrorEvent {
83
114
  provider: ProviderName;
84
115
  paymentMethodType?: PaymentMethodType;
116
+ phase: CheckoutErrorPhase;
117
+ /** Provider decline code when it exposes one, for mapping to a failure reason. */
118
+ declineCode?: string | null;
119
+ message?: string;
85
120
  error: unknown;
86
121
  }
87
122
  interface CheckoutCancelEvent {
88
123
  provider: ProviderName;
89
124
  paymentMethodType?: PaymentMethodType;
90
125
  }
126
+ interface CheckoutReadyEvent {
127
+ provider: ProviderName;
128
+ /** Milliseconds from render() to the checkout UI being usable. */
129
+ durationMs: number;
130
+ }
131
+ /** The buyer authorized the payment; the charge is not confirmed yet. */
132
+ interface CheckoutConfirmEvent {
133
+ provider: ProviderName;
134
+ paymentMethodType?: PaymentMethodType;
135
+ /** Authorized through a wallet button rather than the pay button. */
136
+ express: boolean;
137
+ }
138
+ /** Latest snapshot of what the buyer selected and what they will be charged. */
139
+ interface CheckoutChangeEvent {
140
+ provider: ProviderName;
141
+ paymentMethodType?: PaymentMethodType;
142
+ email?: string;
143
+ /** Currency actually charged — Adaptive Pricing may differ from the listed one. */
144
+ currency?: string;
145
+ amountTotal?: number;
146
+ complete?: boolean;
147
+ }
91
148
 
92
149
  interface CheckoutCallbacks {
93
150
  onComplete?: (event: CheckoutCompleteEvent) => void;
94
151
  onError?: (event: CheckoutErrorEvent) => void;
95
152
  onCancel?: (event: CheckoutCancelEvent) => void;
153
+ onReady?: (event: CheckoutReadyEvent) => void;
154
+ onConfirm?: (event: CheckoutConfirmEvent) => void;
155
+ onChange?: (event: CheckoutChangeEvent) => void;
96
156
  }
97
157
 
98
158
  interface RenderOptions {
@@ -123,9 +183,9 @@ declare class CheckoutError extends Error {
123
183
  constructor(message: string);
124
184
  }
125
185
 
126
- type CreateCheckoutSessionParams = Omit<StripeConfig, 'publishableKey' | 'returnUrl'> & {
186
+ type CreateCheckoutSessionParams = Omit<StripeConfig, 'publishableKey' | 'returnUrl' | 'form'> & {
127
187
  returnUrl: string;
128
188
  };
129
189
  declare function buildStripeCheckoutSessionBody(params: CreateCheckoutSessionParams): Record<string, unknown>;
130
190
 
131
- export { type CheckoutCallbacks, type CheckoutCancelEvent, type CheckoutCompleteEvent, CheckoutError, type CheckoutErrorEvent, type CheckoutInstance, type CreateCheckoutSessionParams, type FastSpringConfig, type PaymentMethodType, type PaymentProvider, type ProviderConfigMap, type ProviderName, type RenderOptions, type StripeCheckoutTrial, type StripeConfig, buildStripeCheckoutSessionBody, initCheckout };
191
+ export { type CheckoutCallbacks, type CheckoutCancelEvent, type CheckoutChangeEvent, type CheckoutCompleteEvent, type CheckoutConfirmEvent, CheckoutError, type CheckoutErrorEvent, type CheckoutErrorPhase, type CheckoutInstance, type CheckoutReadyEvent, type CreateCheckoutSessionParams, type FastSpringConfig, type PaymentMethodType, type PaymentProvider, type ProviderConfigMap, type ProviderName, type RenderOptions, type StripeCheckoutTrial, type StripeConfig, type StripeFormOptions, type StripeUiMode, buildStripeCheckoutSessionBody, initCheckout };
package/dist/index.js CHANGED
@@ -34,7 +34,8 @@ function buildStripeCheckoutSessionBody(params) {
34
34
  }
35
35
  },
36
36
  ...params.upgrade && { upgrade: params.upgrade },
37
- ...params.walletFirst && { wallet_first: params.walletFirst }
37
+ ...params.walletFirst && { wallet_first: params.walletFirst },
38
+ ...params.uiMode && { ui_mode: params.uiMode }
38
39
  };
39
40
  }
40
41
 
@@ -97,55 +98,195 @@ var StripeCheckoutService = class extends HttpService {
97
98
  }
98
99
  };
99
100
 
101
+ // src/providers/stripe/ui/embedded-page.ts
102
+ var StripeEmbeddedPageUi = class {
103
+ checkout = null;
104
+ async mount(stripe, container, host) {
105
+ try {
106
+ this.checkout = await stripe.createEmbeddedCheckoutPage({
107
+ fetchClientSecret: () => host.clientSecret(),
108
+ onComplete: () => host.succeeded()
109
+ });
110
+ } catch (err) {
111
+ host.renderFailed(err, { message: err instanceof Error ? err.message : void 0 });
112
+ if (err instanceof CheckoutError) throw err;
113
+ const detail = err instanceof Error ? err.message : String(err);
114
+ throw new CheckoutError(`Stripe checkout failed: ${detail}`);
115
+ }
116
+ this.checkout.mount(container);
117
+ }
118
+ destroy() {
119
+ this.checkout?.destroy();
120
+ this.checkout = null;
121
+ }
122
+ };
123
+
124
+ // src/providers/stripe/ui/form.ts
125
+ var CHECKOUT_FORM_BETA = "custom_checkout_payment_form_1";
126
+ var StripeFormUi = class {
127
+ constructor(options = {}) {
128
+ this.options = options;
129
+ }
130
+ options;
131
+ loadOptions = { betas: [CHECKOUT_FORM_BETA] };
132
+ checkout = null;
133
+ form = null;
134
+ async mount(stripe, container, host) {
135
+ const { appearance, fonts, loader, layout, expressCheckout } = this.options;
136
+ const startedAt = Date.now();
137
+ try {
138
+ if (typeof stripe.initCheckoutFormSdk !== "function") {
139
+ throw new CheckoutError("Stripe has not granted this account access to Checkout Form");
140
+ }
141
+ this.checkout = stripe.initCheckoutFormSdk({
142
+ clientSecret: host.clientSecret(),
143
+ ...appearance && { appearance },
144
+ ...fonts?.length && { fonts },
145
+ ...loader && { loader }
146
+ });
147
+ this.checkout.on("change", (session) => host.changed(fromSession(session)));
148
+ this.form = this.checkout.createForm({
149
+ ...layout && { layout },
150
+ ...expressCheckout && { expressCheckout }
151
+ });
152
+ this.form.on("ready", () => host.ready(Date.now() - startedAt));
153
+ this.form.on(
154
+ "loaderror",
155
+ (event) => host.renderFailed(event.error, { message: event.error.message })
156
+ );
157
+ this.form.on("change", (event) => host.changed(fromForm(event)));
158
+ this.form.on("cancel", () => host.cancelled());
159
+ this.form.mount(container);
160
+ const actions = await this.loadActions();
161
+ this.form.on("confirm", (event) => void this.confirm(actions, event, host));
162
+ } catch (err) {
163
+ host.renderFailed(err, { message: err instanceof Error ? err.message : void 0 });
164
+ if (err instanceof CheckoutError) throw err;
165
+ const detail = err instanceof Error ? err.message : String(err);
166
+ throw new CheckoutError(`Stripe checkout failed: ${detail}`);
167
+ }
168
+ }
169
+ destroy() {
170
+ this.form?.destroy();
171
+ this.form = null;
172
+ this.checkout = null;
173
+ }
174
+ async loadActions() {
175
+ const result = await this.checkout.loadActions();
176
+ if (result.type !== "success") {
177
+ throw new CheckoutError(`Stripe checkout failed: ${result.error.message}`);
178
+ }
179
+ return result.actions;
180
+ }
181
+ async confirm(actions, event, host) {
182
+ const express = event.source === "checkout-form-ece";
183
+ const paymentMethodType = express ? void 0 : event.paymentMethodType ?? void 0;
184
+ host.confirmed({ paymentMethodType, express });
185
+ try {
186
+ const result = await actions.confirm({ formConfirmEvent: event, redirect: "if_required" });
187
+ if (result.type === "error") {
188
+ host.paymentFailed(result.error, {
189
+ declineCode: result.error.code === "paymentFailed" ? result.error.paymentFailed.declineCode : null,
190
+ message: result.error.message
191
+ });
192
+ return;
193
+ }
194
+ host.succeeded(fromSession(result.session));
195
+ } catch (err) {
196
+ host.paymentFailed(err, { message: err instanceof Error ? err.message : void 0 });
197
+ }
198
+ }
199
+ };
200
+ function fromSession(session) {
201
+ return {
202
+ email: session.email ?? void 0,
203
+ currency: session.currency,
204
+ amountTotal: session.total?.total?.minorUnitsAmount
205
+ };
206
+ }
207
+ function fromForm(event) {
208
+ const { payment, customerDetails, adaptivePricing } = event.value;
209
+ return {
210
+ paymentMethodType: payment?.type,
211
+ email: customerDetails?.email || void 0,
212
+ currency: adaptivePricing?.currency,
213
+ complete: event.complete
214
+ };
215
+ }
216
+
217
+ // src/providers/stripe/ui/index.ts
218
+ function createStripeCheckoutUi(config) {
219
+ switch (config.uiMode) {
220
+ case "form":
221
+ return new StripeFormUi(config.form);
222
+ default:
223
+ return new StripeEmbeddedPageUi();
224
+ }
225
+ }
226
+
100
227
  // src/providers/stripe/index.ts
101
228
  var StripeProvider = class {
102
229
  constructor(config, callbacks, baseUrl, headers) {
103
230
  this.config = config;
104
231
  this.callbacks = callbacks;
105
232
  this.service = new StripeCheckoutService(baseUrl, headers);
233
+ this.ui = createStripeCheckoutUi(config);
106
234
  }
107
235
  config;
108
236
  callbacks;
109
- stripe = null;
110
- embeddedCheckout = null;
111
237
  service;
238
+ ui;
239
+ stripe = null;
240
+ mounted = false;
112
241
  clientSecret = null;
242
+ snapshot = {};
113
243
  async init() {
114
- const stripe = await loadStripe(this.config.publishableKey);
244
+ const stripe = await loadStripe(this.config.publishableKey, this.ui.loadOptions);
115
245
  if (!stripe) {
116
- throw new CheckoutError("Failed to load Stripe.js \u2014 verify your publishableKey");
246
+ const error = new CheckoutError("Failed to load Stripe.js \u2014 verify your publishableKey");
247
+ this.emitError("init", error, { message: error.message });
248
+ throw error;
117
249
  }
118
250
  this.stripe = stripe;
119
251
  }
120
- async render(container, options) {
252
+ async render(container, _options) {
121
253
  if (!this.stripe) {
122
254
  throw new CheckoutError("Provider not initialized \u2014 call init() before render()");
123
255
  }
124
- if (this.embeddedCheckout) {
256
+ if (this.mounted) {
125
257
  throw new CheckoutError("Already rendered \u2014 call destroy() before rendering again");
126
258
  }
127
- try {
128
- this.embeddedCheckout = await this.stripe.createEmbeddedCheckoutPage({
129
- fetchClientSecret: () => this.fetchClientSecret(),
130
- onComplete: () => this.handleComplete(),
131
- onAnalyticsEvent: (event) => console.log("[stripe analytics]", event.eventType, event)
132
- });
133
- } catch (err) {
134
- this.callbacks.onError?.({ provider: "stripe", error: err });
135
- if (err instanceof CheckoutError) throw err;
136
- const detail = err instanceof Error ? err.message : String(err);
137
- throw new CheckoutError(`Stripe checkout failed: ${detail}`);
138
- }
139
- this.embeddedCheckout.mount(container);
259
+ this.mounted = true;
260
+ await this.ui.mount(this.stripe, container, this.host());
140
261
  }
141
262
  async reset() {
142
263
  this.destroy();
143
264
  }
144
265
  destroy() {
145
- this.embeddedCheckout?.destroy();
146
- this.embeddedCheckout = null;
266
+ this.ui.destroy();
267
+ this.mounted = false;
268
+ this.snapshot = {};
147
269
  }
148
- async fetchClientSecret() {
270
+ host() {
271
+ return {
272
+ clientSecret: () => this.createSession(),
273
+ ready: (durationMs) => this.callbacks.onReady?.({ provider: "stripe", durationMs }),
274
+ changed: (patch) => this.merge(patch),
275
+ confirmed: ({ paymentMethodType, express }) => this.callbacks.onConfirm?.({
276
+ provider: "stripe",
277
+ paymentMethodType: paymentMethodType ?? this.snapshot.paymentMethodType,
278
+ express
279
+ }),
280
+ cancelled: () => this.callbacks.onCancel?.({
281
+ provider: "stripe",
282
+ paymentMethodType: this.snapshot.paymentMethodType
283
+ }),
284
+ renderFailed: (error, detail) => this.emitError("render", error, detail),
285
+ paymentFailed: (error, detail) => this.emitError("payment", error, detail),
286
+ succeeded: (outcome) => void this.emitComplete(outcome)
287
+ };
288
+ }
289
+ async createSession() {
149
290
  const secret = await this.service.createCheckoutSession({
150
291
  ...this.config,
151
292
  returnUrl: this.config.returnUrl ?? window.location.href
@@ -154,33 +295,74 @@ var StripeProvider = class {
154
295
  return secret;
155
296
  }
156
297
  // Format: cs_test_{sessionId}_secret_{random}
157
- parseSessionId(clientSecret) {
158
- if (!clientSecret) return null;
159
- const idx = clientSecret.indexOf("_secret_");
160
- return idx !== -1 ? clientSecret.slice(0, idx) : null;
161
- }
162
- async handleComplete() {
163
- const sessionId = this.parseSessionId(this.clientSecret);
164
- if (!sessionId) {
165
- this.callbacks.onComplete?.({ provider: "stripe" });
166
- return;
298
+ sessionId() {
299
+ if (!this.clientSecret) {
300
+ return null;
167
301
  }
168
- try {
169
- const session = await this.service.getSession(sessionId, this.config.accountId);
170
- this.callbacks.onComplete?.({
171
- provider: "stripe",
172
- customerId: session.customer_id ?? void 0,
173
- subscriptionId: session.subscription_id ?? void 0,
174
- invoiceId: session.invoice_id ?? void 0,
175
- amountPaid: session.amount_total ?? void 0,
176
- currency: session.currency ?? void 0,
177
- email: session.email ?? void 0
178
- });
179
- } catch {
180
- this.callbacks.onComplete?.({ provider: "stripe" });
302
+ const idx = this.clientSecret.indexOf("_secret_");
303
+ return idx !== -1 ? this.clientSecret.slice(0, idx) : null;
304
+ }
305
+ merge(patch) {
306
+ const next = { ...this.snapshot };
307
+ for (const [key, value] of Object.entries(patch)) {
308
+ if (value !== void 0) {
309
+ Object.assign(next, { [key]: value });
310
+ }
311
+ }
312
+ this.snapshot = next;
313
+ this.callbacks.onChange?.({ provider: "stripe", ...next });
314
+ }
315
+ /**
316
+ * Reads the session back for the ids only the backend can resolve, falling
317
+ * back to what the client already knows so a paid customer is never
318
+ * reported as a failure.
319
+ */
320
+ async emitComplete(outcome) {
321
+ const sessionId = this.sessionId();
322
+ const fallback = {
323
+ amount_total: outcome?.amountTotal ?? null,
324
+ currency: outcome?.currency ?? null,
325
+ email: outcome?.email ?? null
326
+ };
327
+ let session = {};
328
+ if (sessionId) {
329
+ try {
330
+ session = definedOnly(await this.service.getSession(sessionId, this.config.accountId));
331
+ } catch {
332
+ session = {};
333
+ }
181
334
  }
335
+ this.callbacks.onComplete?.({
336
+ provider: "stripe",
337
+ paymentMethodType: this.snapshot.paymentMethodType,
338
+ ...toCompleteFields({ ...fallback, ...session })
339
+ });
340
+ }
341
+ emitError(phase, error, detail) {
342
+ this.callbacks.onError?.({
343
+ provider: "stripe",
344
+ phase,
345
+ paymentMethodType: this.snapshot.paymentMethodType,
346
+ error,
347
+ ...detail
348
+ });
182
349
  }
183
350
  };
351
+ function definedOnly(session) {
352
+ return Object.fromEntries(
353
+ Object.entries(session).filter(([, value]) => value !== null && value !== void 0)
354
+ );
355
+ }
356
+ function toCompleteFields(session) {
357
+ return {
358
+ customerId: session.customer_id ?? void 0,
359
+ subscriptionId: session.subscription_id ?? void 0,
360
+ invoiceId: session.invoice_id ?? void 0,
361
+ amountPaid: session.amount_total ?? void 0,
362
+ currency: session.currency ?? void 0,
363
+ email: session.email ?? void 0
364
+ };
365
+ }
184
366
 
185
367
  // src/providers/fastspring/index.ts
186
368
  var FastSpringProvider = class {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelfox/payments-sdk",
3
- "version": "0.1.2-beta.9",
3
+ "version": "0.2.0-beta.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",