@flopay/js 1.0.3 → 1.1.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.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
 
@@ -603,9 +604,9 @@ var PaymentAPI = class {
603
604
  /**
604
605
  * Fetch and normalize a checkout session.
605
606
  *
606
- * Reads the backend's `gateway` field to determine the provider,
607
- * then wraps the session in a `NormalizedCheckoutSession` for
608
- * provider-agnostic consumption.
607
+ * Reads the backend's `gateways` map to enumerate provider-specific data,
608
+ * then wraps the session in a `NormalizedCheckoutSession` for provider-
609
+ * agnostic consumption.
609
610
  */
610
611
  async getUnifiedCheckoutSession(checkoutSessionId) {
611
612
  const res = await this.getCheckoutSession(checkoutSessionId);
@@ -616,16 +617,18 @@ var PaymentAPI = class {
616
617
  *
617
618
  * The backend will either succeed, return `type: '3ds_required'`
618
619
  * (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
620
+ *
621
+ * @param userId Vestigial — backend's GatewayInterceptor routes via session,
622
+ * not headers, so this value is no longer sent on the wire. Kept in the
623
+ * signature for back-compat with existing callers; will be removed in a
624
+ * future major version.
619
625
  */
620
- async processPayment(userId, data, options) {
626
+ async processPayment(_userId, data, options) {
621
627
  const response = await fetch(
622
628
  `${this.baseUrl}/v1/checkouts/sessions/process`,
623
629
  {
624
630
  method: "POST",
625
- headers: {
626
- "Content-Type": "application/json",
627
- "x-user-id": userId
628
- },
631
+ headers: { "Content-Type": "application/json" },
629
632
  body: JSON.stringify(data)
630
633
  }
631
634
  );
@@ -702,19 +705,28 @@ var PaymentAPI = class {
702
705
  * Falls back to create + GET if the backend doesn't support `expand`.
703
706
  */
704
707
  async createAndFetchSession(params) {
708
+ const wireProducts = params.products ?? foldIntoProducts(params.items, params.subscriptions);
705
709
  const sessionCurrency = resolveSessionCurrency(
706
710
  params.currency,
707
711
  params.items,
708
- params.subscriptions
712
+ params.subscriptions,
713
+ wireProducts
709
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
+ }
710
722
  const payload = {
711
723
  clientId: params.clientId,
724
+ checkoutVersion: SDK_VERSION,
712
725
  successUrl: params.successUrl,
713
726
  cancelUrl: params.cancelUrl,
714
727
  currency: sessionCurrency,
715
728
  checkoutMode: params.checkoutMode ?? "full",
716
- items: (params.items ?? []).map((item) => buildItemPayload(item, sessionCurrency)),
717
- subscriptions: (params.subscriptions ?? []).map((sub) => buildSubscriptionPayload(sub, sessionCurrency)),
729
+ products: wireProducts.map((product) => buildProductPayload(product, sessionCurrency)),
718
730
  accountData: {
719
731
  userId: params.account.userId,
720
732
  firstName: params.account.firstName ?? null,
@@ -756,7 +768,7 @@ var PaymentAPI = class {
756
768
  throw await buildApiErrorFromResponse(response, "Failed to create checkout session");
757
769
  }
758
770
  const body = await response.json();
759
- if (body.data && "gateway" in body.data) {
771
+ if (body.data && "gateways" in body.data) {
760
772
  this.autoCacheDisplayData(body.data.uuid, params);
761
773
  const merged = this.mergeCachedDisplayData(body.data);
762
774
  return {
@@ -809,57 +821,57 @@ var PaymentAPI = class {
809
821
  }
810
822
  /** Normalize a raw session into a provider-agnostic shape. */
811
823
  normalizeRawSession(session) {
812
- const gateway = session.gateway;
813
- if (gateway === "chargebee") {
814
- return {
815
- provider: "chargebee",
816
- mode: "tokenize",
817
- data: { session: this.toCheckoutSession(session) },
818
- raw: { data: session }
819
- };
820
- }
821
- if (gateway === "stripe") {
824
+ const gateways = session.gateways ?? {};
825
+ const providers = [];
826
+ const data = {
827
+ session: this.toCheckoutSession(session)
828
+ };
829
+ const stripeGateway = gateways.stripe;
830
+ if (stripeGateway?.publishableKey) {
831
+ providers.push("stripe");
822
832
  const rawSession = session;
823
- const gatewayDataRecord = session.gatewayData ?? {};
824
833
  const stripeClientSecret = [
825
834
  rawSession["stripeClientSecret"],
826
- gatewayDataRecord["stripeClientSecret"],
827
- gatewayDataRecord["clientSecret"]
835
+ stripeGateway.stripeClientSecret
828
836
  ].find((value) => typeof value === "string" && value.length > 0);
829
- return {
830
- provider: "stripe",
831
- mode: "tokenize",
832
- data: {
833
- session: this.toCheckoutSession(session),
834
- stripe: {
835
- clientSecret: stripeClientSecret ?? "",
836
- publishableKey: session.gatewayData?.publishableKey ?? void 0,
837
- paypalPublishableKey: session.gatewayData?.paypalPublishableKey ?? void 0
838
- }
839
- },
840
- raw: { data: session }
837
+ data.stripe = {
838
+ clientSecret: stripeClientSecret ?? "",
839
+ publishableKey: stripeGateway.publishableKey ?? void 0,
840
+ paypalPublishableKey: stripeGateway.paypalPublishableKey ?? void 0,
841
+ environment: stripeGateway.environment
842
+ };
843
+ }
844
+ const paypalGateway = gateways.paypal;
845
+ if (paypalGateway?.publishableKey) {
846
+ providers.push("paypal");
847
+ data.paypal = {
848
+ publishableKey: paypalGateway.publishableKey,
849
+ environment: paypalGateway.environment
841
850
  };
842
851
  }
843
852
  return {
844
- provider: "recurly",
853
+ providers,
845
854
  mode: "tokenize",
846
- data: { session: this.toCheckoutSession(session) },
855
+ data,
847
856
  raw: { data: session }
848
857
  };
849
858
  }
850
859
  /** Convert raw session to the SDK CheckoutSession shape. */
851
860
  toCheckoutSession(raw) {
852
- const totalAmount = [
853
- ...raw.subscriptions.map((s) => s.overrideAmount ?? s.totalAmount ?? 0),
854
- ...raw.items.map((i) => i.overrideAmount ?? i.totalAmount ?? 0)
855
- ].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;
856
868
  const amountInCents = Math.round(totalAmount * 100);
857
- 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";
858
871
  return {
859
- // Core fields (backward compat)
860
872
  id: raw.uuid,
861
873
  clientSecret: raw.nonce,
862
- mode: raw.subscriptions.length > 0 ? "subscription" : "payment",
874
+ mode,
863
875
  status: this.toCheckoutSessionStatus(raw.status),
864
876
  amount: amountInCents,
865
877
  currency,
@@ -877,16 +889,22 @@ var PaymentAPI = class {
877
889
  line2: raw.accountData.addressLine2 ?? void 0
878
890
  },
879
891
  metadata: {},
880
- // Full session data from billing API
881
892
  checkoutMode: raw.checkoutMode,
882
- items: raw.items,
883
- 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
+ })),
884
900
  successUrl: raw.successUrl,
885
901
  cancelUrl: raw.cancelUrl,
886
902
  coupons: raw.coupons,
903
+ subtotalAmount: raw.subtotalAmount,
904
+ discountAmount: raw.discountAmount,
905
+ totalAmount: raw.totalAmount,
887
906
  createdAt: raw.createdAt,
888
- gateway: raw.gateway,
889
- gatewayData: raw.gatewayData,
907
+ gateways: raw.gateways,
890
908
  accountData: raw.accountData,
891
909
  tagsData: raw.tagsData
892
910
  };
@@ -943,70 +961,63 @@ var PaymentAPI = class {
943
961
  * Stash the display-only fields the consumer passed into a create-session
944
962
  * call. Runs after the backend assigns a UUID so a later GET on the same
945
963
  * session (typically after a redirect) can fill in fields the backend no
946
- * longer persists — `overrideAmount`, `totalAmount`, `itemName`, etc.
964
+ * longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
947
965
  *
948
966
  * No-op when no UUID is available.
949
967
  */
950
968
  autoCacheDisplayData(sessionId, params) {
951
969
  if (!sessionId) return;
952
- 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) {
953
972
  return;
954
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
+ );
955
981
  cacheSessionDisplayData(sessionId, {
956
- currency: params.currency,
957
- items: params.items,
958
- 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
+ }))
959
991
  });
960
992
  }
961
993
  /**
962
994
  * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
963
- * into a raw session response and mirror the new/legacy name aliases so
964
- * readers using either field always get a value when one exists.
965
- *
966
- * Server values always win — cache fills in only where the server returned
967
- * `null` / `undefined`.
995
+ * into a raw session response. Server values always win cache fills in
996
+ * only where the server returned `null` / `undefined`.
968
997
  */
969
998
  mergeCachedDisplayData(raw) {
970
999
  const cached = getSessionDisplayData(raw.uuid);
971
- const cachedItems = /* @__PURE__ */ new Map();
972
- for (const item of cached?.items ?? []) {
973
- const key = item.code ?? item.providerItemId;
974
- if (key) cachedItems.set(key, item);
975
- }
976
- const cachedSubs = /* @__PURE__ */ new Map();
977
- for (const sub of cached?.subscriptions ?? []) {
978
- const key = sub.code ?? sub.providerPlanId;
979
- if (key) cachedSubs.set(key, sub);
980
- }
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
+ });
981
1017
  return {
982
1018
  ...raw,
983
1019
  currency: raw.currency ?? cached?.currency,
984
- items: raw.items.map((item) => {
985
- const key = item.code ?? item.providerItemId;
986
- const fallback = key ? cachedItems.get(key) : void 0;
987
- const resolvedName = item.itemName ?? item.providerItemName ?? fallback?.itemName ?? fallback?.providerItemName;
988
- return {
989
- ...item,
990
- itemName: resolvedName,
991
- providerItemName: resolvedName,
992
- totalAmount: item.totalAmount ?? fallback?.totalAmount,
993
- overrideAmount: item.overrideAmount ?? fallback?.overrideAmount,
994
- currency: item.currency ?? fallback?.currency
995
- };
996
- }),
997
- subscriptions: raw.subscriptions.map((sub) => {
998
- const key = sub.code ?? sub.providerPlanId;
999
- const fallback = key ? cachedSubs.get(key) : void 0;
1000
- const resolvedName = sub.subscriptionName ?? sub.providerPlanName ?? fallback?.subscriptionName ?? fallback?.providerPlanName;
1001
- return {
1002
- ...sub,
1003
- subscriptionName: resolvedName,
1004
- providerPlanName: resolvedName,
1005
- totalAmount: sub.totalAmount ?? fallback?.totalAmount,
1006
- overrideAmount: sub.overrideAmount ?? fallback?.overrideAmount,
1007
- currency: sub.currency ?? fallback?.currency
1008
- };
1009
- })
1020
+ products: mergedProducts
1010
1021
  };
1011
1022
  }
1012
1023
  };
@@ -1143,16 +1154,39 @@ async function loadFloPay(publishableKey, options) {
1143
1154
 
1144
1155
  // src/create-checkout-session.ts
1145
1156
  import {
1146
- buildItemPayload as buildItemPayload2,
1147
- buildSubscriptionPayload as buildSubscriptionPayload2,
1157
+ FloPayError as FloPayError6,
1158
+ SDK_VERSION as SDK_VERSION2,
1159
+ buildProductPayload as buildProductPayload2,
1160
+ foldIntoProducts as foldIntoProducts2,
1148
1161
  resolveSessionCurrency as resolveSessionCurrency2
1149
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
+ }
1150
1183
  async function createCheckoutSession(options) {
1151
1184
  const {
1152
1185
  billingApiUrl,
1153
1186
  checkoutBaseUrl,
1154
1187
  items = [],
1155
1188
  subscriptions = [],
1189
+ products,
1156
1190
  account,
1157
1191
  successUrl,
1158
1192
  cancelUrl,
@@ -1166,15 +1200,30 @@ async function createCheckoutSession(options) {
1166
1200
  currency,
1167
1201
  utmMetadata
1168
1202
  } = options;
1169
- 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
+ }
1170
1219
  const payload = {
1171
1220
  clientId,
1221
+ checkoutVersion: SDK_VERSION2,
1172
1222
  successUrl,
1173
1223
  cancelUrl,
1174
1224
  currency: sessionCurrency,
1175
1225
  checkoutMode,
1176
- items: items.map((item) => buildItemPayload2(item, sessionCurrency)),
1177
- subscriptions: subscriptions.map((sub) => buildSubscriptionPayload2(sub, sessionCurrency)),
1226
+ products: wireProducts.map((product) => buildProductPayload2(product, sessionCurrency)),
1178
1227
  accountData: {
1179
1228
  userId: account.userId,
1180
1229
  firstName: account.firstName ?? null,
@@ -1216,16 +1265,25 @@ async function createCheckoutSession(options) {
1216
1265
  } finally {
1217
1266
  clearTimeout(timer);
1218
1267
  }
1268
+ if (status >= 400) {
1269
+ throw buildCheckoutSessionError(status, body);
1270
+ }
1219
1271
  if (status === 201) {
1220
1272
  const uuid = body?.data?.uuid;
1221
1273
  if (!uuid) {
1222
1274
  throw new Error("Checkout session created but no UUID was returned by the billing API");
1223
1275
  }
1224
- if (items.length || subscriptions.length || currency) {
1276
+ if (wireProducts.length || sessionCurrency) {
1225
1277
  cacheSessionDisplayData(uuid, {
1226
- currency,
1227
- items,
1228
- 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
+ }))
1229
1287
  });
1230
1288
  }
1231
1289
  const redirectUrl = new URL(`${checkoutBaseUrl.replace(/\/+$/, "")}/secure`);