@flopay/js 1.1.0 → 1.1.4

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
@@ -167,30 +167,14 @@ declare class StripeAdapter implements PaymentProviderAdapter {
167
167
  * Values from the server response always win — cached values fill in only
168
168
  * where the server returned `null` or `undefined`.
169
169
  */
170
- /** Display-only fields per item that can be cached and merged back later. */
171
- interface SessionDisplayItem {
172
- /** Catalog code (preferred match key). */
170
+ /** Display-only fields per product that can be cached and merged back later. */
171
+ interface SessionDisplayProduct {
172
+ /** Catalog code (match key). */
173
173
  code?: string;
174
- /** @deprecated Match key fallback when `code` is not provided. */
175
- providerItemId?: string;
176
- /** Display-only name for the item. Takes priority over `providerItemName`. */
177
- itemName?: string | null;
178
- /** @deprecated Use `itemName`. */
179
- providerItemName?: string | null;
180
- totalAmount?: number;
181
- overrideAmount?: number | null;
182
- currency?: string;
183
- }
184
- /** Display-only fields per subscription that can be cached and merged back later. */
185
- interface SessionDisplaySubscription {
186
- /** Catalog code (preferred match key). */
187
- code?: string;
188
- /** @deprecated Match key fallback when `code` is not provided. */
189
- providerPlanId?: string;
190
- /** Display-only name for the subscription plan. Takes priority over `providerPlanName`. */
191
- subscriptionName?: string | null;
192
- /** @deprecated Use `subscriptionName`. */
193
- providerPlanName?: string | null;
174
+ /** Whether this product is a one-time item or a recurring subscription. */
175
+ type?: 'item' | 'subscription';
176
+ /** Display-only name for the product. */
177
+ name?: string | null;
194
178
  totalAmount?: number;
195
179
  overrideAmount?: number | null;
196
180
  currency?: string;
@@ -199,8 +183,7 @@ interface SessionDisplaySubscription {
199
183
  interface SessionDisplayCacheData {
200
184
  /** Session-level currency (falls into the response only when the server omits it). */
201
185
  currency?: string;
202
- items?: SessionDisplayItem[];
203
- subscriptions?: SessionDisplaySubscription[];
186
+ products?: SessionDisplayProduct[];
204
187
  }
205
188
  /**
206
189
  * Stash display-only data for a session. Called client-side right after the
@@ -232,57 +215,35 @@ interface RawCheckoutSession {
232
215
  status: 'pending' | 'completed' | 'expired';
233
216
  successUrl: string;
234
217
  cancelUrl: string;
235
- /** Session-level currency. Takes precedence over per-item/per-subscription currency. */
218
+ /** Session-level currency. */
236
219
  currency?: string;
237
220
  createdAt?: string;
238
221
  checkoutUrl?: string;
239
- items: Array<{
240
- uuid: string;
241
- checkoutSessionId: string;
242
- /** Preferred catalog code. Falls back to the deprecated `providerItemId`. */
243
- code?: string;
244
- /** @deprecated Use `code`. */
245
- providerItemId?: string;
246
- /** Display-only name. Preferred over `providerItemName`. */
247
- itemName?: string | null;
248
- /** @deprecated Use `itemName`. Mirrored for backward compatibility. */
249
- providerItemName?: string | null;
250
- /** Display-only description from the catalog. */
251
- providerItemDescription?: string | null;
252
- quantity: number;
253
- /** @deprecated Removed from the backend — resolved from the catalog. */
254
- totalAmount?: number;
255
- /** @deprecated Removed from the backend entirely. */
256
- overrideAmount?: number | null;
257
- /** @deprecated Use the session-level `currency`. */
258
- currency?: string;
259
- metadata?: Record<string, unknown> | null;
260
- }>;
261
- subscriptions: Array<{
222
+ /** Unified products list returned by the billing API (post-#760). */
223
+ products?: Array<{
262
224
  uuid: string;
263
225
  checkoutSessionId: string;
264
- /** Preferred catalog code. Falls back to the deprecated `providerPlanId`. */
226
+ /** 'item' or 'subscription'. */
227
+ type: 'item' | 'subscription';
265
228
  code?: string;
266
- /** @deprecated Use `code`. */
267
- providerPlanId?: string;
268
- /** Display-only name. Preferred over `providerPlanName`. */
269
- subscriptionName?: string | null;
270
- /** @deprecated Use `subscriptionName`. Mirrored for backward compatibility. */
271
- providerPlanName?: string | null;
272
- /** Display-only description from the catalog. */
273
- providerPlanDescription?: string | null;
229
+ name?: string | null;
230
+ description?: string | null;
274
231
  quantity: number;
275
- /** @deprecated Removed from the backend — resolved from the catalog. */
276
232
  totalAmount?: number;
277
- /** @deprecated Removed from the backend entirely. */
278
233
  overrideAmount?: number | null;
279
- /** @deprecated Use the session-level `currency`. */
280
234
  currency?: string;
281
- /** @deprecated Removed from the backend. Checkouts only create new subscriptions. */
282
- isUpdate?: boolean;
283
235
  metadata?: Record<string, unknown> | null;
284
236
  }>;
285
237
  coupons?: string[];
238
+ /**
239
+ * Pre-discount total in cart-currency major units. Populated by billing
240
+ * API ≥ v1.1.2; absent on older backends.
241
+ */
242
+ subtotalAmount?: number;
243
+ /** Total reduction from applied coupons (cart-currency major units). */
244
+ discountAmount?: number;
245
+ /** Final charge amount after coupon discount (cart-currency major units). */
246
+ totalAmount?: number;
286
247
  checkoutMode?: 'full' | 'auto' | 'confirm';
287
248
  gateways?: CheckoutGateways;
288
249
  accountData: {
@@ -421,18 +382,15 @@ declare class PaymentAPI {
421
382
  * Stash the display-only fields the consumer passed into a create-session
422
383
  * call. Runs after the backend assigns a UUID so a later GET on the same
423
384
  * session (typically after a redirect) can fill in fields the backend no
424
- * longer persists — `overrideAmount`, `totalAmount`, `itemName`, etc.
385
+ * longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
425
386
  *
426
387
  * No-op when no UUID is available.
427
388
  */
428
389
  private autoCacheDisplayData;
429
390
  /**
430
391
  * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
431
- * into a raw session response and mirror the new/legacy name aliases so
432
- * readers using either field always get a value when one exists.
433
- *
434
- * Server values always win — cache fills in only where the server returned
435
- * `null` / `undefined`.
392
+ * into a raw session response. Server values always win cache fills in
393
+ * only where the server returned `null` / `undefined`.
436
394
  */
437
395
  private mergeCachedDisplayData;
438
396
  }
@@ -478,4 +436,4 @@ declare function createCheckoutSessionWithRetries(options: CreateSessionParams &
478
436
  maxRetries?: number;
479
437
  }): Promise<CheckoutSessionResult>;
480
438
 
481
- export { FloPay, FloPayElements, PaymentAPI, type SessionDisplayCacheData, type SessionDisplayItem, type SessionDisplaySubscription, StripeAdapter, cacheSessionDisplayData, clearSessionDisplayData, createCheckoutSession, createCheckoutSessionWithRetries, getSessionDisplayData, loadFloPay };
439
+ export { FloPay, FloPayElements, PaymentAPI, type SessionDisplayCacheData, type SessionDisplayProduct, StripeAdapter, cacheSessionDisplayData, clearSessionDisplayData, createCheckoutSession, createCheckoutSessionWithRetries, getSessionDisplayData, loadFloPay };
package/dist/index.mjs CHANGED
@@ -449,8 +449,9 @@ var FloPayElements = class {
449
449
  // src/payment-api.ts
450
450
  import {
451
451
  FloPayError as FloPayError3,
452
- buildItemPayload,
453
- buildSubscriptionPayload,
452
+ SDK_VERSION,
453
+ buildProductPayload,
454
+ foldIntoProducts,
454
455
  resolveSessionCurrency
455
456
  } from "@flopay/shared";
456
457
 
@@ -704,19 +705,28 @@ var PaymentAPI = class {
704
705
  * Falls back to create + GET if the backend doesn't support `expand`.
705
706
  */
706
707
  async createAndFetchSession(params) {
708
+ const wireProducts = params.products ?? foldIntoProducts(params.items, params.subscriptions);
707
709
  const sessionCurrency = resolveSessionCurrency(
708
710
  params.currency,
709
711
  params.items,
710
- params.subscriptions
712
+ params.subscriptions,
713
+ wireProducts
711
714
  );
715
+ if (!sessionCurrency) {
716
+ throw new FloPayError3(
717
+ "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
718
+ "validation_error",
719
+ { code: "CurrencyRequired", param: "currency" }
720
+ );
721
+ }
712
722
  const payload = {
713
723
  clientId: params.clientId,
724
+ checkoutVersion: SDK_VERSION,
714
725
  successUrl: params.successUrl,
715
726
  cancelUrl: params.cancelUrl,
716
727
  currency: sessionCurrency,
717
728
  checkoutMode: params.checkoutMode ?? "full",
718
- items: (params.items ?? []).map((item) => buildItemPayload(item, sessionCurrency)),
719
- subscriptions: (params.subscriptions ?? []).map((sub) => buildSubscriptionPayload(sub, sessionCurrency)),
729
+ products: wireProducts.map((product) => buildProductPayload(product, sessionCurrency)),
720
730
  accountData: {
721
731
  userId: params.account.userId,
722
732
  firstName: params.account.firstName ?? null,
@@ -848,17 +858,20 @@ var PaymentAPI = class {
848
858
  }
849
859
  /** Convert raw session to the SDK CheckoutSession shape. */
850
860
  toCheckoutSession(raw) {
851
- const totalAmount = [
852
- ...raw.subscriptions.map((s) => s.overrideAmount ?? s.totalAmount ?? 0),
853
- ...raw.items.map((i) => i.overrideAmount ?? i.totalAmount ?? 0)
854
- ].reduce((sum, val) => sum + val, 0);
861
+ const rawProducts = raw.products ?? [];
862
+ const hasBackendTotal = typeof raw.totalAmount === "number" && Number.isFinite(raw.totalAmount);
863
+ const computedTotal = rawProducts.reduce(
864
+ (sum, p) => sum + (p.overrideAmount ?? p.totalAmount ?? 0),
865
+ 0
866
+ );
867
+ const totalAmount = hasBackendTotal ? raw.totalAmount : computedTotal;
855
868
  const amountInCents = Math.round(totalAmount * 100);
856
- const currency = raw.currency ?? raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD";
869
+ const currency = raw.currency ?? rawProducts[0]?.currency ?? "USD";
870
+ const mode = rawProducts.some((p) => p.type === "subscription") ? "subscription" : "payment";
857
871
  return {
858
- // Core fields (backward compat)
859
872
  id: raw.uuid,
860
873
  clientSecret: raw.nonce,
861
- mode: raw.subscriptions.length > 0 ? "subscription" : "payment",
874
+ mode,
862
875
  status: this.toCheckoutSessionStatus(raw.status),
863
876
  amount: amountInCents,
864
877
  currency,
@@ -876,13 +889,20 @@ var PaymentAPI = class {
876
889
  line2: raw.accountData.addressLine2 ?? void 0
877
890
  },
878
891
  metadata: {},
879
- // Full session data from billing API
880
892
  checkoutMode: raw.checkoutMode,
881
- items: raw.items,
882
- subscriptions: raw.subscriptions,
893
+ products: rawProducts.map((p) => ({
894
+ ...p,
895
+ totalAmount: typeof p.totalAmount === "number" ? p.totalAmount : void 0,
896
+ overrideAmount: typeof p.overrideAmount === "number" ? p.overrideAmount : null,
897
+ currency: typeof p.currency === "string" ? p.currency : void 0,
898
+ metadata: p.metadata ?? null
899
+ })),
883
900
  successUrl: raw.successUrl,
884
901
  cancelUrl: raw.cancelUrl,
885
902
  coupons: raw.coupons,
903
+ subtotalAmount: raw.subtotalAmount,
904
+ discountAmount: raw.discountAmount,
905
+ totalAmount: raw.totalAmount,
886
906
  createdAt: raw.createdAt,
887
907
  gateways: raw.gateways,
888
908
  accountData: raw.accountData,
@@ -941,70 +961,63 @@ var PaymentAPI = class {
941
961
  * Stash the display-only fields the consumer passed into a create-session
942
962
  * call. Runs after the backend assigns a UUID so a later GET on the same
943
963
  * session (typically after a redirect) can fill in fields the backend no
944
- * longer persists — `overrideAmount`, `totalAmount`, `itemName`, etc.
964
+ * longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
945
965
  *
946
966
  * No-op when no UUID is available.
947
967
  */
948
968
  autoCacheDisplayData(sessionId, params) {
949
969
  if (!sessionId) return;
950
- if (!params.items?.length && !params.subscriptions?.length && !params.currency) {
970
+ const products = params.products ?? foldIntoProducts(params.items, params.subscriptions);
971
+ if (products.length === 0 && !params.currency) {
951
972
  return;
952
973
  }
974
+ const usingUnifiedProducts = params.products !== void 0;
975
+ const sessionCurrency = resolveSessionCurrency(
976
+ params.currency,
977
+ usingUnifiedProducts ? void 0 : params.items,
978
+ usingUnifiedProducts ? void 0 : params.subscriptions,
979
+ products
980
+ );
953
981
  cacheSessionDisplayData(sessionId, {
954
- currency: params.currency,
955
- items: params.items,
956
- subscriptions: params.subscriptions
982
+ currency: sessionCurrency ?? void 0,
983
+ products: products.map((p) => ({
984
+ code: p.code ?? p.providerItemId ?? p.providerPlanId,
985
+ type: p.type,
986
+ name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
987
+ totalAmount: p.totalAmount,
988
+ overrideAmount: p.overrideAmount,
989
+ currency: p.currency ?? sessionCurrency ?? void 0
990
+ }))
957
991
  });
958
992
  }
959
993
  /**
960
994
  * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
961
- * into a raw session response and mirror the new/legacy name aliases so
962
- * readers using either field always get a value when one exists.
963
- *
964
- * Server values always win — cache fills in only where the server returned
965
- * `null` / `undefined`.
995
+ * into a raw session response. Server values always win cache fills in
996
+ * only where the server returned `null` / `undefined`.
966
997
  */
967
998
  mergeCachedDisplayData(raw) {
968
999
  const cached = getSessionDisplayData(raw.uuid);
969
- const cachedItems = /* @__PURE__ */ new Map();
970
- for (const item of cached?.items ?? []) {
971
- const key = item.code ?? item.providerItemId;
972
- if (key) cachedItems.set(key, item);
973
- }
974
- const cachedSubs = /* @__PURE__ */ new Map();
975
- for (const sub of cached?.subscriptions ?? []) {
976
- const key = sub.code ?? sub.providerPlanId;
977
- if (key) cachedSubs.set(key, sub);
978
- }
1000
+ const cachedProducts = /* @__PURE__ */ new Map();
1001
+ const productKey = (type, code) => code && type ? `${type}:${code}` : void 0;
1002
+ for (const p of cached?.products ?? []) {
1003
+ const key = productKey(p.type, p.code);
1004
+ if (key) cachedProducts.set(key, p);
1005
+ }
1006
+ const mergedProducts = (raw.products ?? []).map((p) => {
1007
+ const key = productKey(p.type, p.code);
1008
+ const fallback = key ? cachedProducts.get(key) : void 0;
1009
+ return {
1010
+ ...p,
1011
+ name: p.name ?? fallback?.name ?? null,
1012
+ totalAmount: p.totalAmount ?? fallback?.totalAmount,
1013
+ overrideAmount: p.overrideAmount ?? fallback?.overrideAmount,
1014
+ currency: p.currency ?? fallback?.currency
1015
+ };
1016
+ });
979
1017
  return {
980
1018
  ...raw,
981
1019
  currency: raw.currency ?? cached?.currency,
982
- items: raw.items.map((item) => {
983
- const key = item.code ?? item.providerItemId;
984
- const fallback = key ? cachedItems.get(key) : void 0;
985
- const resolvedName = item.itemName ?? item.providerItemName ?? fallback?.itemName ?? fallback?.providerItemName;
986
- return {
987
- ...item,
988
- itemName: resolvedName,
989
- providerItemName: resolvedName,
990
- totalAmount: item.totalAmount ?? fallback?.totalAmount,
991
- overrideAmount: item.overrideAmount ?? fallback?.overrideAmount,
992
- currency: item.currency ?? fallback?.currency
993
- };
994
- }),
995
- subscriptions: raw.subscriptions.map((sub) => {
996
- const key = sub.code ?? sub.providerPlanId;
997
- const fallback = key ? cachedSubs.get(key) : void 0;
998
- const resolvedName = sub.subscriptionName ?? sub.providerPlanName ?? fallback?.subscriptionName ?? fallback?.providerPlanName;
999
- return {
1000
- ...sub,
1001
- subscriptionName: resolvedName,
1002
- providerPlanName: resolvedName,
1003
- totalAmount: sub.totalAmount ?? fallback?.totalAmount,
1004
- overrideAmount: sub.overrideAmount ?? fallback?.overrideAmount,
1005
- currency: sub.currency ?? fallback?.currency
1006
- };
1007
- })
1020
+ products: mergedProducts
1008
1021
  };
1009
1022
  }
1010
1023
  };
@@ -1141,16 +1154,39 @@ async function loadFloPay(publishableKey, options) {
1141
1154
 
1142
1155
  // src/create-checkout-session.ts
1143
1156
  import {
1144
- buildItemPayload as buildItemPayload2,
1145
- buildSubscriptionPayload as buildSubscriptionPayload2,
1157
+ FloPayError as FloPayError6,
1158
+ SDK_VERSION as SDK_VERSION2,
1159
+ buildProductPayload as buildProductPayload2,
1160
+ foldIntoProducts as foldIntoProducts2,
1146
1161
  resolveSessionCurrency as resolveSessionCurrency2
1147
1162
  } from "@flopay/shared";
1163
+ var MAX_COUPON_CODES = 5;
1164
+ function readString2(value) {
1165
+ return typeof value === "string" && value.trim() ? value : void 0;
1166
+ }
1167
+ function buildCheckoutSessionError(status, payload) {
1168
+ const nested = payload?.error;
1169
+ const code = readString2(payload?.code) ?? readString2(nested?.code) ?? `http_${status}`;
1170
+ const message = readString2(payload?.message) ?? readString2(nested?.message) ?? defaultMessageForCode(code, status);
1171
+ return new FloPayError6(message, "api_error", { code, statusCode: status });
1172
+ }
1173
+ function defaultMessageForCode(code, status) {
1174
+ switch (code) {
1175
+ case "CouponLimitExceeded":
1176
+ return `Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`;
1177
+ case "CouponCurrencyUnsupported":
1178
+ return "One of the applied coupons has no price configured for the cart currency.";
1179
+ default:
1180
+ return `Failed to create checkout session (HTTP ${status}).`;
1181
+ }
1182
+ }
1148
1183
  async function createCheckoutSession(options) {
1149
1184
  const {
1150
1185
  billingApiUrl,
1151
1186
  checkoutBaseUrl,
1152
1187
  items = [],
1153
1188
  subscriptions = [],
1189
+ products,
1154
1190
  account,
1155
1191
  successUrl,
1156
1192
  cancelUrl,
@@ -1164,15 +1200,30 @@ async function createCheckoutSession(options) {
1164
1200
  currency,
1165
1201
  utmMetadata
1166
1202
  } = options;
1167
- const sessionCurrency = resolveSessionCurrency2(currency, items, subscriptions);
1203
+ if (couponCodes.length > MAX_COUPON_CODES) {
1204
+ throw new FloPayError6(
1205
+ `Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
1206
+ "validation_error",
1207
+ { code: "CouponLimitExceeded", param: "couponCodes" }
1208
+ );
1209
+ }
1210
+ const wireProducts = products ?? foldIntoProducts2(items, subscriptions);
1211
+ const sessionCurrency = resolveSessionCurrency2(currency, items, subscriptions, wireProducts);
1212
+ if (!sessionCurrency) {
1213
+ throw new FloPayError6(
1214
+ "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
1215
+ "validation_error",
1216
+ { code: "CurrencyRequired", param: "currency" }
1217
+ );
1218
+ }
1168
1219
  const payload = {
1169
1220
  clientId,
1221
+ checkoutVersion: SDK_VERSION2,
1170
1222
  successUrl,
1171
1223
  cancelUrl,
1172
1224
  currency: sessionCurrency,
1173
1225
  checkoutMode,
1174
- items: items.map((item) => buildItemPayload2(item, sessionCurrency)),
1175
- subscriptions: subscriptions.map((sub) => buildSubscriptionPayload2(sub, sessionCurrency)),
1226
+ products: wireProducts.map((product) => buildProductPayload2(product, sessionCurrency)),
1176
1227
  accountData: {
1177
1228
  userId: account.userId,
1178
1229
  firstName: account.firstName ?? null,
@@ -1214,16 +1265,25 @@ async function createCheckoutSession(options) {
1214
1265
  } finally {
1215
1266
  clearTimeout(timer);
1216
1267
  }
1268
+ if (status >= 400) {
1269
+ throw buildCheckoutSessionError(status, body);
1270
+ }
1217
1271
  if (status === 201) {
1218
1272
  const uuid = body?.data?.uuid;
1219
1273
  if (!uuid) {
1220
1274
  throw new Error("Checkout session created but no UUID was returned by the billing API");
1221
1275
  }
1222
- if (items.length || subscriptions.length || currency) {
1276
+ if (wireProducts.length || sessionCurrency) {
1223
1277
  cacheSessionDisplayData(uuid, {
1224
- currency,
1225
- items,
1226
- subscriptions
1278
+ currency: sessionCurrency,
1279
+ products: wireProducts.map((p) => ({
1280
+ code: p.code ?? p.providerItemId ?? p.providerPlanId,
1281
+ type: p.type,
1282
+ name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
1283
+ totalAmount: p.totalAmount,
1284
+ overrideAmount: p.overrideAmount,
1285
+ currency: p.currency ?? sessionCurrency
1286
+ }))
1227
1287
  });
1228
1288
  }
1229
1289
  const redirectUrl = new URL(`${checkoutBaseUrl.replace(/\/+$/, "")}/secure`);