@flopay/js 0.5.19 → 1.0.2

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.d.ts CHANGED
@@ -155,6 +155,73 @@ declare class StripeAdapter implements PaymentProviderAdapter {
155
155
  destroy(): void;
156
156
  }
157
157
 
158
+ /**
159
+ * Client-side cache for display-only checkout fields the backend no longer
160
+ * persists (`overrideAmount`, `totalAmount`, `providerItemName`,
161
+ * `providerPlanName`, per-line `currency`).
162
+ *
163
+ * The cache lives in `sessionStorage` so it survives the navigation from the
164
+ * page that creates the session to the checkout page that fetches it, but
165
+ * dies on tab close. An in-memory fallback keeps the SDK working in Node /
166
+ * SSR contexts where `sessionStorage` is unavailable.
167
+ *
168
+ * Values from the server response always win — cached values fill in only
169
+ * where the server returned `null` or `undefined`.
170
+ */
171
+ /** Display-only fields per item that can be cached and merged back later. */
172
+ interface SessionDisplayItem {
173
+ /** Catalog code (preferred match key). */
174
+ code?: string;
175
+ /** @deprecated Match key fallback when `code` is not provided. */
176
+ providerItemId?: string;
177
+ /** Display-only name for the item. Takes priority over `providerItemName`. */
178
+ itemName?: string | null;
179
+ /** @deprecated Use `itemName`. */
180
+ providerItemName?: string | null;
181
+ totalAmount?: number;
182
+ overrideAmount?: number | null;
183
+ currency?: string;
184
+ }
185
+ /** Display-only fields per subscription that can be cached and merged back later. */
186
+ interface SessionDisplaySubscription {
187
+ /** Catalog code (preferred match key). */
188
+ code?: string;
189
+ /** @deprecated Match key fallback when `code` is not provided. */
190
+ providerPlanId?: string;
191
+ /** Display-only name for the subscription plan. Takes priority over `providerPlanName`. */
192
+ subscriptionName?: string | null;
193
+ /** @deprecated Use `subscriptionName`. */
194
+ providerPlanName?: string | null;
195
+ totalAmount?: number;
196
+ overrideAmount?: number | null;
197
+ currency?: string;
198
+ }
199
+ /** Display-only payload that can be stashed for later merge into a session response. */
200
+ interface SessionDisplayCacheData {
201
+ /** Session-level currency (falls into the response only when the server omits it). */
202
+ currency?: string;
203
+ items?: SessionDisplayItem[];
204
+ subscriptions?: SessionDisplaySubscription[];
205
+ }
206
+ /**
207
+ * Stash display-only data for a session. Called client-side right after the
208
+ * server returns a session ID, so the values survive the redirect to the
209
+ * checkout page.
210
+ */
211
+ declare function cacheSessionDisplayData(sessionId: string, data: SessionDisplayCacheData, options?: {
212
+ ttlMs?: number;
213
+ }): void;
214
+ /**
215
+ * Read previously-cached display data for a session, or `null` if nothing
216
+ * is cached (or the TTL has elapsed).
217
+ */
218
+ declare function getSessionDisplayData(sessionId: string): SessionDisplayCacheData | null;
219
+ /**
220
+ * Drop any cached display data for a session. Call from the success page
221
+ * after the payment completes; otherwise the TTL handles cleanup.
222
+ */
223
+ declare function clearSessionDisplayData(sessionId: string): void;
224
+
158
225
  /** Raw billing API response wrapper. */
159
226
  interface BillingResponse<T> {
160
227
  data: T;
@@ -167,30 +234,53 @@ interface RawCheckoutSession {
167
234
  status: 'pending' | 'completed' | 'expired';
168
235
  successUrl: string;
169
236
  cancelUrl: string;
237
+ /** Session-level currency. Takes precedence over per-item/per-subscription currency. */
238
+ currency?: string;
170
239
  createdAt?: string;
171
240
  checkoutUrl?: string;
172
241
  items: Array<{
173
242
  uuid: string;
174
243
  checkoutSessionId: string;
175
- providerItemId: string;
176
- providerItemName: string;
244
+ /** Preferred catalog code. Falls back to the deprecated `providerItemId`. */
245
+ code?: string;
246
+ /** @deprecated Use `code`. */
247
+ providerItemId?: string;
248
+ /** Display-only name. Preferred over `providerItemName`. */
249
+ itemName?: string | null;
250
+ /** @deprecated Use `itemName`. Mirrored for backward compatibility. */
251
+ providerItemName?: string | null;
252
+ /** Display-only description from the catalog. */
177
253
  providerItemDescription?: string | null;
178
254
  quantity: number;
179
- totalAmount: number;
180
- overrideAmount: number | null;
181
- currency: string;
255
+ /** @deprecated Removed from the backend — resolved from the catalog. */
256
+ totalAmount?: number;
257
+ /** @deprecated Removed from the backend entirely. */
258
+ overrideAmount?: number | null;
259
+ /** @deprecated Use the session-level `currency`. */
260
+ currency?: string;
182
261
  metadata?: Record<string, unknown> | null;
183
262
  }>;
184
263
  subscriptions: Array<{
185
264
  uuid: string;
186
265
  checkoutSessionId: string;
187
- providerPlanId: string;
188
- providerPlanName: string;
266
+ /** Preferred catalog code. Falls back to the deprecated `providerPlanId`. */
267
+ code?: string;
268
+ /** @deprecated Use `code`. */
269
+ providerPlanId?: string;
270
+ /** Display-only name. Preferred over `providerPlanName`. */
271
+ subscriptionName?: string | null;
272
+ /** @deprecated Use `subscriptionName`. Mirrored for backward compatibility. */
273
+ providerPlanName?: string | null;
274
+ /** Display-only description from the catalog. */
189
275
  providerPlanDescription?: string | null;
190
276
  quantity: number;
191
- totalAmount: number;
192
- overrideAmount: number | null;
193
- currency: string;
277
+ /** @deprecated Removed from the backend — resolved from the catalog. */
278
+ totalAmount?: number;
279
+ /** @deprecated Removed from the backend entirely. */
280
+ overrideAmount?: number | null;
281
+ /** @deprecated Use the session-level `currency`. */
282
+ currency?: string;
283
+ /** @deprecated Removed from the backend. Checkouts only create new subscriptions. */
194
284
  isUpdate?: boolean;
195
285
  metadata?: Record<string, unknown> | null;
196
286
  }>;
@@ -232,6 +322,33 @@ declare class PaymentAPI {
232
322
  constructor(billingApiUrl: string);
233
323
  /** Fetch a raw checkout session by ID. */
234
324
  getCheckoutSession(checkoutSessionId: string): Promise<BillingResponse<RawCheckoutSession>>;
325
+ /**
326
+ * Stash display-only data for a session so subsequent fetches can fill in
327
+ * fields the backend no longer persists (`overrideAmount`, `totalAmount`,
328
+ * `providerItemName`, `providerPlanName`).
329
+ *
330
+ * Backed by `sessionStorage` in the browser, with an in-memory fallback in
331
+ * Node/SSR contexts. Default TTL: 1 hour.
332
+ *
333
+ * Server-returned values always win — cached values fill in only where the
334
+ * server returned `null` / `undefined`.
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * paymentAPI.cacheSessionDisplayData(sessionId, {
339
+ * currency: 'USD',
340
+ * items: [{ code: 'pro_plan', overrideAmount: 24.99, providerItemName: 'Pro' }],
341
+ * });
342
+ * ```
343
+ */
344
+ cacheSessionDisplayData(sessionId: string, data: SessionDisplayCacheData, options?: {
345
+ ttlMs?: number;
346
+ }): void;
347
+ /**
348
+ * Drop any cached display data for a session. Call after the payment
349
+ * completes; otherwise the TTL handles cleanup.
350
+ */
351
+ clearSessionDisplayData(sessionId: string): void;
235
352
  /**
236
353
  * Fetch and normalize a checkout session.
237
354
  *
@@ -301,6 +418,15 @@ declare class PaymentAPI {
301
418
  private resolveProcessResponse;
302
419
  private toCheckoutProcessingPending;
303
420
  private clampRetryAfterMs;
421
+ /**
422
+ * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
423
+ * into a raw session response and mirror the new/legacy name aliases so
424
+ * readers using either field always get a value when one exists.
425
+ *
426
+ * Server values always win — cache fills in only where the server returned
427
+ * `null` / `undefined`.
428
+ */
429
+ private mergeCachedDisplayData;
304
430
  }
305
431
 
306
432
  /**
@@ -321,11 +447,11 @@ declare class PaymentAPI {
321
447
  * billingApiUrl: 'https://billing.example.com',
322
448
  * checkoutBaseUrl: 'https://checkout.example.com',
323
449
  * clientId: 'client_123',
450
+ * currency: 'USD',
324
451
  * items: [{
325
- * providerItemId: 'prod_abc',
326
- * providerItemName: 'Pro Plan',
327
- * totalAmount: 49.99,
328
- * overrideAmount: 24.99,
452
+ * code: 'initial_charge',
453
+ * quantity: 1,
454
+ * metadata: { source: 'web' },
329
455
  * }],
330
456
  * account: { userId: 'user_1', email: 'user@example.com' },
331
457
  * successUrl: '/success',
@@ -344,4 +470,4 @@ declare function createCheckoutSessionWithRetries(options: CreateSessionParams &
344
470
  maxRetries?: number;
345
471
  }): Promise<CheckoutSessionResult>;
346
472
 
347
- export { FloPay, FloPayElements, PaymentAPI, StripeAdapter, createCheckoutSession, createCheckoutSessionWithRetries, loadFloPay };
473
+ export { FloPay, FloPayElements, PaymentAPI, type SessionDisplayCacheData, type SessionDisplayItem, type SessionDisplaySubscription, StripeAdapter, cacheSessionDisplayData, clearSessionDisplayData, createCheckoutSession, createCheckoutSessionWithRetries, getSessionDisplayData, loadFloPay };
package/dist/index.mjs CHANGED
@@ -447,7 +447,80 @@ var FloPayElements = class {
447
447
  };
448
448
 
449
449
  // src/payment-api.ts
450
- import { FloPayError as FloPayError3 } from "@flopay/shared";
450
+ import {
451
+ FloPayError as FloPayError3,
452
+ buildItemPayload,
453
+ buildSubscriptionPayload,
454
+ resolveSessionCurrency
455
+ } from "@flopay/shared";
456
+
457
+ // src/session-display-cache.ts
458
+ var STORAGE_KEY_PREFIX = "flopay_session_display:";
459
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
460
+ var memoryStore = /* @__PURE__ */ new Map();
461
+ function storageKey(sessionId) {
462
+ return `${STORAGE_KEY_PREFIX}${sessionId}`;
463
+ }
464
+ function getSessionStorage() {
465
+ if (typeof window === "undefined") return null;
466
+ try {
467
+ return window.sessionStorage;
468
+ } catch {
469
+ return null;
470
+ }
471
+ }
472
+ function cacheSessionDisplayData(sessionId, data, options) {
473
+ if (!sessionId) return;
474
+ const ttl = options?.ttlMs ?? DEFAULT_TTL_MS;
475
+ const entry = { data, expiresAt: Date.now() + ttl };
476
+ const storage = getSessionStorage();
477
+ if (storage) {
478
+ try {
479
+ storage.setItem(storageKey(sessionId), JSON.stringify(entry));
480
+ return;
481
+ } catch {
482
+ }
483
+ }
484
+ memoryStore.set(sessionId, entry);
485
+ }
486
+ function getSessionDisplayData(sessionId) {
487
+ if (!sessionId) return null;
488
+ const storage = getSessionStorage();
489
+ if (storage) {
490
+ try {
491
+ const raw = storage.getItem(storageKey(sessionId));
492
+ if (raw) {
493
+ const entry = JSON.parse(raw);
494
+ if (entry && typeof entry.expiresAt === "number" && entry.expiresAt > Date.now()) {
495
+ return entry.data;
496
+ }
497
+ storage.removeItem(storageKey(sessionId));
498
+ }
499
+ } catch {
500
+ }
501
+ }
502
+ const memEntry = memoryStore.get(sessionId);
503
+ if (memEntry) {
504
+ if (memEntry.expiresAt > Date.now()) {
505
+ return memEntry.data;
506
+ }
507
+ memoryStore.delete(sessionId);
508
+ }
509
+ return null;
510
+ }
511
+ function clearSessionDisplayData(sessionId) {
512
+ if (!sessionId) return;
513
+ memoryStore.delete(sessionId);
514
+ const storage = getSessionStorage();
515
+ if (storage) {
516
+ try {
517
+ storage.removeItem(storageKey(sessionId));
518
+ } catch {
519
+ }
520
+ }
521
+ }
522
+
523
+ // src/payment-api.ts
451
524
  var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
452
525
  var MIN_PROCESSING_RETRY_AFTER_MS = 500;
453
526
  var DEFAULT_PROCESSING_TIMEOUT_MS = 15e3;
@@ -495,7 +568,37 @@ var PaymentAPI = class {
495
568
  if (!response.ok) {
496
569
  throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
497
570
  }
498
- return response.json();
571
+ const body = await response.json();
572
+ return { ...body, data: this.mergeCachedDisplayData(body.data) };
573
+ }
574
+ /**
575
+ * Stash display-only data for a session so subsequent fetches can fill in
576
+ * fields the backend no longer persists (`overrideAmount`, `totalAmount`,
577
+ * `providerItemName`, `providerPlanName`).
578
+ *
579
+ * Backed by `sessionStorage` in the browser, with an in-memory fallback in
580
+ * Node/SSR contexts. Default TTL: 1 hour.
581
+ *
582
+ * Server-returned values always win — cached values fill in only where the
583
+ * server returned `null` / `undefined`.
584
+ *
585
+ * @example
586
+ * ```ts
587
+ * paymentAPI.cacheSessionDisplayData(sessionId, {
588
+ * currency: 'USD',
589
+ * items: [{ code: 'pro_plan', overrideAmount: 24.99, providerItemName: 'Pro' }],
590
+ * });
591
+ * ```
592
+ */
593
+ cacheSessionDisplayData(sessionId, data, options) {
594
+ cacheSessionDisplayData(sessionId, data, options);
595
+ }
596
+ /**
597
+ * Drop any cached display data for a session. Call after the payment
598
+ * completes; otherwise the TTL handles cleanup.
599
+ */
600
+ clearSessionDisplayData(sessionId) {
601
+ clearSessionDisplayData(sessionId);
499
602
  }
500
603
  /**
501
604
  * Fetch and normalize a checkout session.
@@ -599,27 +702,19 @@ var PaymentAPI = class {
599
702
  * Falls back to create + GET if the backend doesn't support `expand`.
600
703
  */
601
704
  async createAndFetchSession(params) {
705
+ const sessionCurrency = resolveSessionCurrency(
706
+ params.currency,
707
+ params.items,
708
+ params.subscriptions
709
+ );
602
710
  const payload = {
603
711
  clientId: params.clientId,
604
712
  successUrl: params.successUrl,
605
713
  cancelUrl: params.cancelUrl,
714
+ currency: sessionCurrency,
606
715
  checkoutMode: params.checkoutMode ?? "full",
607
- items: (params.items ?? []).map((item) => ({
608
- providerItemId: item.providerItemId,
609
- providerItemName: item.providerItemName ?? null,
610
- quantity: item.quantity ?? 1,
611
- totalAmount: item.totalAmount,
612
- overrideAmount: item.overrideAmount ?? null,
613
- currency: item.currency ?? "USD"
614
- })),
615
- subscriptions: (params.subscriptions ?? []).map((sub) => ({
616
- providerPlanId: sub.providerPlanId,
617
- providerPlanName: sub.providerPlanName ?? null,
618
- quantity: sub.quantity ?? 1,
619
- totalAmount: sub.totalAmount,
620
- overrideAmount: sub.overrideAmount ?? null,
621
- currency: sub.currency ?? "USD"
622
- })),
716
+ items: (params.items ?? []).map((item) => buildItemPayload(item, sessionCurrency)),
717
+ subscriptions: (params.subscriptions ?? []).map((sub) => buildSubscriptionPayload(sub, sessionCurrency)),
623
718
  accountData: {
624
719
  userId: params.account.userId,
625
720
  firstName: params.account.firstName ?? null,
@@ -662,8 +757,9 @@ var PaymentAPI = class {
662
757
  }
663
758
  const body = await response.json();
664
759
  if (body.data && "gateway" in body.data) {
760
+ const merged = this.mergeCachedDisplayData(body.data);
665
761
  return {
666
- ...this.normalizeRawSession(body.data),
762
+ ...this.normalizeRawSession(merged),
667
763
  autoProcessingError: body.autoProcessingError,
668
764
  autoProcessingAttempted: body.autoProcessingAttempted,
669
765
  autoProcessingPending: body.autoProcessingPending
@@ -752,10 +848,11 @@ var PaymentAPI = class {
752
848
  /** Convert raw session to the SDK CheckoutSession shape. */
753
849
  toCheckoutSession(raw) {
754
850
  const totalAmount = [
755
- ...raw.subscriptions.map((s) => s.overrideAmount || s.totalAmount),
756
- ...raw.items.map((i) => i.overrideAmount || i.totalAmount)
851
+ ...raw.subscriptions.map((s) => s.overrideAmount ?? s.totalAmount ?? 0),
852
+ ...raw.items.map((i) => i.overrideAmount ?? i.totalAmount ?? 0)
757
853
  ].reduce((sum, val) => sum + val, 0);
758
854
  const amountInCents = Math.round(totalAmount * 100);
855
+ const currency = raw.currency ?? raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD";
759
856
  return {
760
857
  // Core fields (backward compat)
761
858
  id: raw.uuid,
@@ -763,7 +860,7 @@ var PaymentAPI = class {
763
860
  mode: raw.subscriptions.length > 0 ? "subscription" : "payment",
764
861
  status: this.toCheckoutSessionStatus(raw.status),
765
862
  amount: amountInCents,
766
- currency: raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD",
863
+ currency,
767
864
  customer: {
768
865
  id: raw.accountData.userId,
769
866
  email: raw.accountData.email,
@@ -840,6 +937,57 @@ var PaymentAPI = class {
840
937
  clampRetryAfterMs(retryAfterMs) {
841
938
  return Math.max(0, Math.min(retryAfterMs, MAX_PROCESSING_RETRY_AFTER_MS));
842
939
  }
940
+ /**
941
+ * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
942
+ * into a raw session response and mirror the new/legacy name aliases so
943
+ * readers using either field always get a value when one exists.
944
+ *
945
+ * Server values always win — cache fills in only where the server returned
946
+ * `null` / `undefined`.
947
+ */
948
+ mergeCachedDisplayData(raw) {
949
+ const cached = getSessionDisplayData(raw.uuid);
950
+ const cachedItems = /* @__PURE__ */ new Map();
951
+ for (const item of cached?.items ?? []) {
952
+ const key = item.code ?? item.providerItemId;
953
+ if (key) cachedItems.set(key, item);
954
+ }
955
+ const cachedSubs = /* @__PURE__ */ new Map();
956
+ for (const sub of cached?.subscriptions ?? []) {
957
+ const key = sub.code ?? sub.providerPlanId;
958
+ if (key) cachedSubs.set(key, sub);
959
+ }
960
+ return {
961
+ ...raw,
962
+ currency: raw.currency ?? cached?.currency,
963
+ items: raw.items.map((item) => {
964
+ const key = item.code ?? item.providerItemId;
965
+ const fallback = key ? cachedItems.get(key) : void 0;
966
+ const resolvedName = item.itemName ?? item.providerItemName ?? fallback?.itemName ?? fallback?.providerItemName;
967
+ return {
968
+ ...item,
969
+ itemName: resolvedName,
970
+ providerItemName: resolvedName,
971
+ totalAmount: item.totalAmount ?? fallback?.totalAmount,
972
+ overrideAmount: item.overrideAmount ?? fallback?.overrideAmount,
973
+ currency: item.currency ?? fallback?.currency
974
+ };
975
+ }),
976
+ subscriptions: raw.subscriptions.map((sub) => {
977
+ const key = sub.code ?? sub.providerPlanId;
978
+ const fallback = key ? cachedSubs.get(key) : void 0;
979
+ const resolvedName = sub.subscriptionName ?? sub.providerPlanName ?? fallback?.subscriptionName ?? fallback?.providerPlanName;
980
+ return {
981
+ ...sub,
982
+ subscriptionName: resolvedName,
983
+ providerPlanName: resolvedName,
984
+ totalAmount: sub.totalAmount ?? fallback?.totalAmount,
985
+ overrideAmount: sub.overrideAmount ?? fallback?.overrideAmount,
986
+ currency: sub.currency ?? fallback?.currency
987
+ };
988
+ })
989
+ };
990
+ }
843
991
  };
844
992
 
845
993
  // src/flopay.ts
@@ -973,6 +1121,11 @@ async function loadFloPay(publishableKey, options) {
973
1121
  }
974
1122
 
975
1123
  // src/create-checkout-session.ts
1124
+ import {
1125
+ buildItemPayload as buildItemPayload2,
1126
+ buildSubscriptionPayload as buildSubscriptionPayload2,
1127
+ resolveSessionCurrency as resolveSessionCurrency2
1128
+ } from "@flopay/shared";
976
1129
  async function createCheckoutSession(options) {
977
1130
  const {
978
1131
  billingApiUrl,
@@ -989,29 +1142,18 @@ async function createCheckoutSession(options) {
989
1142
  setCookie = true,
990
1143
  timeoutMs = 12e3,
991
1144
  clientId,
1145
+ currency,
992
1146
  utmMetadata
993
1147
  } = options;
1148
+ const sessionCurrency = resolveSessionCurrency2(currency, items, subscriptions);
994
1149
  const payload = {
995
1150
  clientId,
996
1151
  successUrl,
997
1152
  cancelUrl,
1153
+ currency: sessionCurrency,
998
1154
  checkoutMode,
999
- items: items.map((item) => ({
1000
- providerItemId: item.providerItemId,
1001
- providerItemName: item.providerItemName ?? null,
1002
- quantity: item.quantity ?? 1,
1003
- totalAmount: item.totalAmount,
1004
- overrideAmount: item.overrideAmount ?? null,
1005
- currency: item.currency ?? "USD"
1006
- })),
1007
- subscriptions: subscriptions.map((sub) => ({
1008
- providerPlanId: sub.providerPlanId,
1009
- providerPlanName: sub.providerPlanName ?? null,
1010
- quantity: sub.quantity ?? 1,
1011
- totalAmount: sub.totalAmount,
1012
- overrideAmount: sub.overrideAmount ?? null,
1013
- currency: sub.currency ?? "USD"
1014
- })),
1155
+ items: items.map((item) => buildItemPayload2(item, sessionCurrency)),
1156
+ subscriptions: subscriptions.map((sub) => buildSubscriptionPayload2(sub, sessionCurrency)),
1015
1157
  accountData: {
1016
1158
  userId: account.userId,
1017
1159
  firstName: account.firstName ?? null,
@@ -1106,8 +1248,11 @@ export {
1106
1248
  FloPayElements,
1107
1249
  PaymentAPI,
1108
1250
  StripeAdapter,
1251
+ cacheSessionDisplayData,
1252
+ clearSessionDisplayData,
1109
1253
  createCheckoutSession,
1110
1254
  createCheckoutSessionWithRetries,
1255
+ getSessionDisplayData,
1111
1256
  loadFloPay
1112
1257
  };
1113
1258
  //# sourceMappingURL=index.mjs.map