@flopay/js 1.0.0 → 1.0.3

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
@@ -34,8 +34,11 @@ __export(index_exports, {
34
34
  FloPayElements: () => FloPayElements,
35
35
  PaymentAPI: () => PaymentAPI,
36
36
  StripeAdapter: () => StripeAdapter,
37
+ cacheSessionDisplayData: () => cacheSessionDisplayData,
38
+ clearSessionDisplayData: () => clearSessionDisplayData,
37
39
  createCheckoutSession: () => createCheckoutSession,
38
40
  createCheckoutSessionWithRetries: () => createCheckoutSessionWithRetries,
41
+ getSessionDisplayData: () => getSessionDisplayData,
39
42
  loadFloPay: () => loadFloPay
40
43
  });
41
44
  module.exports = __toCommonJS(index_exports);
@@ -490,6 +493,74 @@ var FloPayElements = class {
490
493
 
491
494
  // src/payment-api.ts
492
495
  var import_shared3 = require("@flopay/shared");
496
+
497
+ // src/session-display-cache.ts
498
+ var STORAGE_KEY_PREFIX = "flopay_session_display:";
499
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
500
+ var memoryStore = /* @__PURE__ */ new Map();
501
+ function storageKey(sessionId) {
502
+ return `${STORAGE_KEY_PREFIX}${sessionId}`;
503
+ }
504
+ function getSessionStorage() {
505
+ if (typeof window === "undefined") return null;
506
+ try {
507
+ return window.sessionStorage;
508
+ } catch {
509
+ return null;
510
+ }
511
+ }
512
+ function cacheSessionDisplayData(sessionId, data, options) {
513
+ if (!sessionId) return;
514
+ const ttl = options?.ttlMs ?? DEFAULT_TTL_MS;
515
+ const entry = { data, expiresAt: Date.now() + ttl };
516
+ const storage = getSessionStorage();
517
+ if (storage) {
518
+ try {
519
+ storage.setItem(storageKey(sessionId), JSON.stringify(entry));
520
+ return;
521
+ } catch {
522
+ }
523
+ }
524
+ memoryStore.set(sessionId, entry);
525
+ }
526
+ function getSessionDisplayData(sessionId) {
527
+ if (!sessionId) return null;
528
+ const storage = getSessionStorage();
529
+ if (storage) {
530
+ try {
531
+ const raw = storage.getItem(storageKey(sessionId));
532
+ if (raw) {
533
+ const entry = JSON.parse(raw);
534
+ if (entry && typeof entry.expiresAt === "number" && entry.expiresAt > Date.now()) {
535
+ return entry.data;
536
+ }
537
+ storage.removeItem(storageKey(sessionId));
538
+ }
539
+ } catch {
540
+ }
541
+ }
542
+ const memEntry = memoryStore.get(sessionId);
543
+ if (memEntry) {
544
+ if (memEntry.expiresAt > Date.now()) {
545
+ return memEntry.data;
546
+ }
547
+ memoryStore.delete(sessionId);
548
+ }
549
+ return null;
550
+ }
551
+ function clearSessionDisplayData(sessionId) {
552
+ if (!sessionId) return;
553
+ memoryStore.delete(sessionId);
554
+ const storage = getSessionStorage();
555
+ if (storage) {
556
+ try {
557
+ storage.removeItem(storageKey(sessionId));
558
+ } catch {
559
+ }
560
+ }
561
+ }
562
+
563
+ // src/payment-api.ts
493
564
  var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
494
565
  var MIN_PROCESSING_RETRY_AFTER_MS = 500;
495
566
  var DEFAULT_PROCESSING_TIMEOUT_MS = 15e3;
@@ -537,7 +608,37 @@ var PaymentAPI = class {
537
608
  if (!response.ok) {
538
609
  throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
539
610
  }
540
- return response.json();
611
+ const body = await response.json();
612
+ return { ...body, data: this.mergeCachedDisplayData(body.data) };
613
+ }
614
+ /**
615
+ * Stash display-only data for a session so subsequent fetches can fill in
616
+ * fields the backend no longer persists (`overrideAmount`, `totalAmount`,
617
+ * `providerItemName`, `providerPlanName`).
618
+ *
619
+ * Backed by `sessionStorage` in the browser, with an in-memory fallback in
620
+ * Node/SSR contexts. Default TTL: 1 hour.
621
+ *
622
+ * Server-returned values always win — cached values fill in only where the
623
+ * server returned `null` / `undefined`.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * paymentAPI.cacheSessionDisplayData(sessionId, {
628
+ * currency: 'USD',
629
+ * items: [{ code: 'pro_plan', overrideAmount: 24.99, providerItemName: 'Pro' }],
630
+ * });
631
+ * ```
632
+ */
633
+ cacheSessionDisplayData(sessionId, data, options) {
634
+ cacheSessionDisplayData(sessionId, data, options);
635
+ }
636
+ /**
637
+ * Drop any cached display data for a session. Call after the payment
638
+ * completes; otherwise the TTL handles cleanup.
639
+ */
640
+ clearSessionDisplayData(sessionId) {
641
+ clearSessionDisplayData(sessionId);
541
642
  }
542
643
  /**
543
644
  * Fetch and normalize a checkout session.
@@ -641,27 +742,19 @@ var PaymentAPI = class {
641
742
  * Falls back to create + GET if the backend doesn't support `expand`.
642
743
  */
643
744
  async createAndFetchSession(params) {
745
+ const sessionCurrency = (0, import_shared3.resolveSessionCurrency)(
746
+ params.currency,
747
+ params.items,
748
+ params.subscriptions
749
+ );
644
750
  const payload = {
645
751
  clientId: params.clientId,
646
752
  successUrl: params.successUrl,
647
753
  cancelUrl: params.cancelUrl,
754
+ currency: sessionCurrency,
648
755
  checkoutMode: params.checkoutMode ?? "full",
649
- items: (params.items ?? []).map((item) => ({
650
- providerItemId: item.providerItemId,
651
- providerItemName: item.providerItemName ?? null,
652
- quantity: item.quantity ?? 1,
653
- totalAmount: item.totalAmount,
654
- overrideAmount: item.overrideAmount ?? null,
655
- currency: item.currency ?? "USD"
656
- })),
657
- subscriptions: (params.subscriptions ?? []).map((sub) => ({
658
- providerPlanId: sub.providerPlanId,
659
- providerPlanName: sub.providerPlanName ?? null,
660
- quantity: sub.quantity ?? 1,
661
- totalAmount: sub.totalAmount,
662
- overrideAmount: sub.overrideAmount ?? null,
663
- currency: sub.currency ?? "USD"
664
- })),
756
+ items: (params.items ?? []).map((item) => (0, import_shared3.buildItemPayload)(item, sessionCurrency)),
757
+ subscriptions: (params.subscriptions ?? []).map((sub) => (0, import_shared3.buildSubscriptionPayload)(sub, sessionCurrency)),
665
758
  accountData: {
666
759
  userId: params.account.userId,
667
760
  firstName: params.account.firstName ?? null,
@@ -704,8 +797,10 @@ var PaymentAPI = class {
704
797
  }
705
798
  const body = await response.json();
706
799
  if (body.data && "gateway" in body.data) {
800
+ this.autoCacheDisplayData(body.data.uuid, params);
801
+ const merged = this.mergeCachedDisplayData(body.data);
707
802
  return {
708
- ...this.normalizeRawSession(body.data),
803
+ ...this.normalizeRawSession(merged),
709
804
  autoProcessingError: body.autoProcessingError,
710
805
  autoProcessingAttempted: body.autoProcessingAttempted,
711
806
  autoProcessingPending: body.autoProcessingPending
@@ -715,6 +810,7 @@ var PaymentAPI = class {
715
810
  if (!uuid) {
716
811
  throw new import_shared3.FloPayError("No session ID returned", "api_error");
717
812
  }
813
+ this.autoCacheDisplayData(uuid, params);
718
814
  const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
719
815
  return {
720
816
  ...unifiedSession,
@@ -794,10 +890,11 @@ var PaymentAPI = class {
794
890
  /** Convert raw session to the SDK CheckoutSession shape. */
795
891
  toCheckoutSession(raw) {
796
892
  const totalAmount = [
797
- ...raw.subscriptions.map((s) => s.overrideAmount || s.totalAmount),
798
- ...raw.items.map((i) => i.overrideAmount || i.totalAmount)
893
+ ...raw.subscriptions.map((s) => s.overrideAmount ?? s.totalAmount ?? 0),
894
+ ...raw.items.map((i) => i.overrideAmount ?? i.totalAmount ?? 0)
799
895
  ].reduce((sum, val) => sum + val, 0);
800
896
  const amountInCents = Math.round(totalAmount * 100);
897
+ const currency = raw.currency ?? raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD";
801
898
  return {
802
899
  // Core fields (backward compat)
803
900
  id: raw.uuid,
@@ -805,7 +902,7 @@ var PaymentAPI = class {
805
902
  mode: raw.subscriptions.length > 0 ? "subscription" : "payment",
806
903
  status: this.toCheckoutSessionStatus(raw.status),
807
904
  amount: amountInCents,
808
- currency: raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD",
905
+ currency,
809
906
  customer: {
810
907
  id: raw.accountData.userId,
811
908
  email: raw.accountData.email,
@@ -882,6 +979,76 @@ var PaymentAPI = class {
882
979
  clampRetryAfterMs(retryAfterMs) {
883
980
  return Math.max(0, Math.min(retryAfterMs, MAX_PROCESSING_RETRY_AFTER_MS));
884
981
  }
982
+ /**
983
+ * Stash the display-only fields the consumer passed into a create-session
984
+ * call. Runs after the backend assigns a UUID so a later GET on the same
985
+ * session (typically after a redirect) can fill in fields the backend no
986
+ * longer persists — `overrideAmount`, `totalAmount`, `itemName`, etc.
987
+ *
988
+ * No-op when no UUID is available.
989
+ */
990
+ autoCacheDisplayData(sessionId, params) {
991
+ if (!sessionId) return;
992
+ if (!params.items?.length && !params.subscriptions?.length && !params.currency) {
993
+ return;
994
+ }
995
+ cacheSessionDisplayData(sessionId, {
996
+ currency: params.currency,
997
+ items: params.items,
998
+ subscriptions: params.subscriptions
999
+ });
1000
+ }
1001
+ /**
1002
+ * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
1003
+ * into a raw session response and mirror the new/legacy name aliases so
1004
+ * readers using either field always get a value when one exists.
1005
+ *
1006
+ * Server values always win — cache fills in only where the server returned
1007
+ * `null` / `undefined`.
1008
+ */
1009
+ mergeCachedDisplayData(raw) {
1010
+ const cached = getSessionDisplayData(raw.uuid);
1011
+ const cachedItems = /* @__PURE__ */ new Map();
1012
+ for (const item of cached?.items ?? []) {
1013
+ const key = item.code ?? item.providerItemId;
1014
+ if (key) cachedItems.set(key, item);
1015
+ }
1016
+ const cachedSubs = /* @__PURE__ */ new Map();
1017
+ for (const sub of cached?.subscriptions ?? []) {
1018
+ const key = sub.code ?? sub.providerPlanId;
1019
+ if (key) cachedSubs.set(key, sub);
1020
+ }
1021
+ return {
1022
+ ...raw,
1023
+ currency: raw.currency ?? cached?.currency,
1024
+ items: raw.items.map((item) => {
1025
+ const key = item.code ?? item.providerItemId;
1026
+ const fallback = key ? cachedItems.get(key) : void 0;
1027
+ const resolvedName = item.itemName ?? item.providerItemName ?? fallback?.itemName ?? fallback?.providerItemName;
1028
+ return {
1029
+ ...item,
1030
+ itemName: resolvedName,
1031
+ providerItemName: resolvedName,
1032
+ totalAmount: item.totalAmount ?? fallback?.totalAmount,
1033
+ overrideAmount: item.overrideAmount ?? fallback?.overrideAmount,
1034
+ currency: item.currency ?? fallback?.currency
1035
+ };
1036
+ }),
1037
+ subscriptions: raw.subscriptions.map((sub) => {
1038
+ const key = sub.code ?? sub.providerPlanId;
1039
+ const fallback = key ? cachedSubs.get(key) : void 0;
1040
+ const resolvedName = sub.subscriptionName ?? sub.providerPlanName ?? fallback?.subscriptionName ?? fallback?.providerPlanName;
1041
+ return {
1042
+ ...sub,
1043
+ subscriptionName: resolvedName,
1044
+ providerPlanName: resolvedName,
1045
+ totalAmount: sub.totalAmount ?? fallback?.totalAmount,
1046
+ overrideAmount: sub.overrideAmount ?? fallback?.overrideAmount,
1047
+ currency: sub.currency ?? fallback?.currency
1048
+ };
1049
+ })
1050
+ };
1051
+ }
885
1052
  };
886
1053
 
887
1054
  // src/flopay.ts
@@ -1015,6 +1182,7 @@ async function loadFloPay(publishableKey, options) {
1015
1182
  }
1016
1183
 
1017
1184
  // src/create-checkout-session.ts
1185
+ var import_shared6 = require("@flopay/shared");
1018
1186
  async function createCheckoutSession(options) {
1019
1187
  const {
1020
1188
  billingApiUrl,
@@ -1031,29 +1199,18 @@ async function createCheckoutSession(options) {
1031
1199
  setCookie = true,
1032
1200
  timeoutMs = 12e3,
1033
1201
  clientId,
1202
+ currency,
1034
1203
  utmMetadata
1035
1204
  } = options;
1205
+ const sessionCurrency = (0, import_shared6.resolveSessionCurrency)(currency, items, subscriptions);
1036
1206
  const payload = {
1037
1207
  clientId,
1038
1208
  successUrl,
1039
1209
  cancelUrl,
1210
+ currency: sessionCurrency,
1040
1211
  checkoutMode,
1041
- items: items.map((item) => ({
1042
- providerItemId: item.providerItemId,
1043
- providerItemName: item.providerItemName ?? null,
1044
- quantity: item.quantity ?? 1,
1045
- totalAmount: item.totalAmount,
1046
- overrideAmount: item.overrideAmount ?? null,
1047
- currency: item.currency ?? "USD"
1048
- })),
1049
- subscriptions: subscriptions.map((sub) => ({
1050
- providerPlanId: sub.providerPlanId,
1051
- providerPlanName: sub.providerPlanName ?? null,
1052
- quantity: sub.quantity ?? 1,
1053
- totalAmount: sub.totalAmount,
1054
- overrideAmount: sub.overrideAmount ?? null,
1055
- currency: sub.currency ?? "USD"
1056
- })),
1212
+ items: items.map((item) => (0, import_shared6.buildItemPayload)(item, sessionCurrency)),
1213
+ subscriptions: subscriptions.map((sub) => (0, import_shared6.buildSubscriptionPayload)(sub, sessionCurrency)),
1057
1214
  accountData: {
1058
1215
  userId: account.userId,
1059
1216
  firstName: account.firstName ?? null,
@@ -1100,6 +1257,13 @@ async function createCheckoutSession(options) {
1100
1257
  if (!uuid) {
1101
1258
  throw new Error("Checkout session created but no UUID was returned by the billing API");
1102
1259
  }
1260
+ if (items.length || subscriptions.length || currency) {
1261
+ cacheSessionDisplayData(uuid, {
1262
+ currency,
1263
+ items,
1264
+ subscriptions
1265
+ });
1266
+ }
1103
1267
  const redirectUrl = new URL(`${checkoutBaseUrl.replace(/\/+$/, "")}/secure`);
1104
1268
  redirectUrl.searchParams.set("id", uuid);
1105
1269
  for (const [key, value] of Object.entries(redirectParams)) {
@@ -1149,8 +1313,11 @@ async function createCheckoutSessionWithRetries(options) {
1149
1313
  FloPayElements,
1150
1314
  PaymentAPI,
1151
1315
  StripeAdapter,
1316
+ cacheSessionDisplayData,
1317
+ clearSessionDisplayData,
1152
1318
  createCheckoutSession,
1153
1319
  createCheckoutSessionWithRetries,
1320
+ getSessionDisplayData,
1154
1321
  loadFloPay
1155
1322
  });
1156
1323
  //# sourceMappingURL=index.cjs.map