@flopay/js 1.1.0 → 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/README.md +21 -0
- package/dist/index.cjs +123 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -70
- package/dist/index.d.ts +28 -70
- package/dist/index.mjs +130 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,6 +154,27 @@ const result = await createCheckoutSession({
|
|
|
154
154
|
|
|
155
155
|
Use `createCheckoutSessionWithRetries` for automatic retry with exponential backoff on timeout errors.
|
|
156
156
|
|
|
157
|
+
Coupon validation errors are surfaced as `FloPayError` with structured `code`:
|
|
158
|
+
|
|
159
|
+
- `CouponLimitExceeded` — more than 5 coupon codes supplied (also enforced client-side before the request leaves the browser).
|
|
160
|
+
- `CouponCurrencyUnsupported` — an amount-based coupon has no price for the cart currency.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { FloPayError } from '@flopay/shared';
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await createCheckoutSession({ /* … */ couponCodes });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (err instanceof FloPayError) {
|
|
169
|
+
if (err.code === 'CouponLimitExceeded') {
|
|
170
|
+
// show "Too many coupons" toast
|
|
171
|
+
} else if (err.code === 'CouponCurrencyUnsupported') {
|
|
172
|
+
// show "Coupon is not valid for this currency" toast
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
157
178
|
## API Reference
|
|
158
179
|
|
|
159
180
|
### Exports
|
package/dist/index.cjs
CHANGED
|
@@ -744,19 +744,28 @@ var PaymentAPI = class {
|
|
|
744
744
|
* Falls back to create + GET if the backend doesn't support `expand`.
|
|
745
745
|
*/
|
|
746
746
|
async createAndFetchSession(params) {
|
|
747
|
+
const wireProducts = params.products ?? (0, import_shared3.foldIntoProducts)(params.items, params.subscriptions);
|
|
747
748
|
const sessionCurrency = (0, import_shared3.resolveSessionCurrency)(
|
|
748
749
|
params.currency,
|
|
749
750
|
params.items,
|
|
750
|
-
params.subscriptions
|
|
751
|
+
params.subscriptions,
|
|
752
|
+
wireProducts
|
|
751
753
|
);
|
|
754
|
+
if (!sessionCurrency) {
|
|
755
|
+
throw new import_shared3.FloPayError(
|
|
756
|
+
"currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
|
|
757
|
+
"validation_error",
|
|
758
|
+
{ code: "CurrencyRequired", param: "currency" }
|
|
759
|
+
);
|
|
760
|
+
}
|
|
752
761
|
const payload = {
|
|
753
762
|
clientId: params.clientId,
|
|
763
|
+
checkoutVersion: import_shared3.SDK_VERSION,
|
|
754
764
|
successUrl: params.successUrl,
|
|
755
765
|
cancelUrl: params.cancelUrl,
|
|
756
766
|
currency: sessionCurrency,
|
|
757
767
|
checkoutMode: params.checkoutMode ?? "full",
|
|
758
|
-
|
|
759
|
-
subscriptions: (params.subscriptions ?? []).map((sub) => (0, import_shared3.buildSubscriptionPayload)(sub, sessionCurrency)),
|
|
768
|
+
products: wireProducts.map((product) => (0, import_shared3.buildProductPayload)(product, sessionCurrency)),
|
|
760
769
|
accountData: {
|
|
761
770
|
userId: params.account.userId,
|
|
762
771
|
firstName: params.account.firstName ?? null,
|
|
@@ -888,17 +897,20 @@ var PaymentAPI = class {
|
|
|
888
897
|
}
|
|
889
898
|
/** Convert raw session to the SDK CheckoutSession shape. */
|
|
890
899
|
toCheckoutSession(raw) {
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
900
|
+
const rawProducts = raw.products ?? [];
|
|
901
|
+
const hasBackendTotal = typeof raw.totalAmount === "number" && Number.isFinite(raw.totalAmount);
|
|
902
|
+
const computedTotal = rawProducts.reduce(
|
|
903
|
+
(sum, p) => sum + (p.overrideAmount ?? p.totalAmount ?? 0),
|
|
904
|
+
0
|
|
905
|
+
);
|
|
906
|
+
const totalAmount = hasBackendTotal ? raw.totalAmount : computedTotal;
|
|
895
907
|
const amountInCents = Math.round(totalAmount * 100);
|
|
896
|
-
const currency = raw.currency ??
|
|
908
|
+
const currency = raw.currency ?? rawProducts[0]?.currency ?? "USD";
|
|
909
|
+
const mode = rawProducts.some((p) => p.type === "subscription") ? "subscription" : "payment";
|
|
897
910
|
return {
|
|
898
|
-
// Core fields (backward compat)
|
|
899
911
|
id: raw.uuid,
|
|
900
912
|
clientSecret: raw.nonce,
|
|
901
|
-
mode
|
|
913
|
+
mode,
|
|
902
914
|
status: this.toCheckoutSessionStatus(raw.status),
|
|
903
915
|
amount: amountInCents,
|
|
904
916
|
currency,
|
|
@@ -916,13 +928,20 @@ var PaymentAPI = class {
|
|
|
916
928
|
line2: raw.accountData.addressLine2 ?? void 0
|
|
917
929
|
},
|
|
918
930
|
metadata: {},
|
|
919
|
-
// Full session data from billing API
|
|
920
931
|
checkoutMode: raw.checkoutMode,
|
|
921
|
-
|
|
922
|
-
|
|
932
|
+
products: rawProducts.map((p) => ({
|
|
933
|
+
...p,
|
|
934
|
+
totalAmount: typeof p.totalAmount === "number" ? p.totalAmount : void 0,
|
|
935
|
+
overrideAmount: typeof p.overrideAmount === "number" ? p.overrideAmount : null,
|
|
936
|
+
currency: typeof p.currency === "string" ? p.currency : void 0,
|
|
937
|
+
metadata: p.metadata ?? null
|
|
938
|
+
})),
|
|
923
939
|
successUrl: raw.successUrl,
|
|
924
940
|
cancelUrl: raw.cancelUrl,
|
|
925
941
|
coupons: raw.coupons,
|
|
942
|
+
subtotalAmount: raw.subtotalAmount,
|
|
943
|
+
discountAmount: raw.discountAmount,
|
|
944
|
+
totalAmount: raw.totalAmount,
|
|
926
945
|
createdAt: raw.createdAt,
|
|
927
946
|
gateways: raw.gateways,
|
|
928
947
|
accountData: raw.accountData,
|
|
@@ -981,70 +1000,63 @@ var PaymentAPI = class {
|
|
|
981
1000
|
* Stash the display-only fields the consumer passed into a create-session
|
|
982
1001
|
* call. Runs after the backend assigns a UUID so a later GET on the same
|
|
983
1002
|
* session (typically after a redirect) can fill in fields the backend no
|
|
984
|
-
* longer persists — `overrideAmount`, `totalAmount`, `
|
|
1003
|
+
* longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
|
|
985
1004
|
*
|
|
986
1005
|
* No-op when no UUID is available.
|
|
987
1006
|
*/
|
|
988
1007
|
autoCacheDisplayData(sessionId, params) {
|
|
989
1008
|
if (!sessionId) return;
|
|
990
|
-
|
|
1009
|
+
const products = params.products ?? (0, import_shared3.foldIntoProducts)(params.items, params.subscriptions);
|
|
1010
|
+
if (products.length === 0 && !params.currency) {
|
|
991
1011
|
return;
|
|
992
1012
|
}
|
|
1013
|
+
const usingUnifiedProducts = params.products !== void 0;
|
|
1014
|
+
const sessionCurrency = (0, import_shared3.resolveSessionCurrency)(
|
|
1015
|
+
params.currency,
|
|
1016
|
+
usingUnifiedProducts ? void 0 : params.items,
|
|
1017
|
+
usingUnifiedProducts ? void 0 : params.subscriptions,
|
|
1018
|
+
products
|
|
1019
|
+
);
|
|
993
1020
|
cacheSessionDisplayData(sessionId, {
|
|
994
|
-
currency:
|
|
995
|
-
|
|
996
|
-
|
|
1021
|
+
currency: sessionCurrency ?? void 0,
|
|
1022
|
+
products: products.map((p) => ({
|
|
1023
|
+
code: p.code ?? p.providerItemId ?? p.providerPlanId,
|
|
1024
|
+
type: p.type,
|
|
1025
|
+
name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
|
|
1026
|
+
totalAmount: p.totalAmount,
|
|
1027
|
+
overrideAmount: p.overrideAmount,
|
|
1028
|
+
currency: p.currency ?? sessionCurrency ?? void 0
|
|
1029
|
+
}))
|
|
997
1030
|
});
|
|
998
1031
|
}
|
|
999
1032
|
/**
|
|
1000
1033
|
* Merge cached display-only fields (set by {@link cacheSessionDisplayData})
|
|
1001
|
-
* into a raw session response
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
* Server values always win — cache fills in only where the server returned
|
|
1005
|
-
* `null` / `undefined`.
|
|
1034
|
+
* into a raw session response. Server values always win — cache fills in
|
|
1035
|
+
* only where the server returned `null` / `undefined`.
|
|
1006
1036
|
*/
|
|
1007
1037
|
mergeCachedDisplayData(raw) {
|
|
1008
1038
|
const cached = getSessionDisplayData(raw.uuid);
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const key =
|
|
1017
|
-
|
|
1018
|
-
|
|
1039
|
+
const cachedProducts = /* @__PURE__ */ new Map();
|
|
1040
|
+
const productKey = (type, code) => code && type ? `${type}:${code}` : void 0;
|
|
1041
|
+
for (const p of cached?.products ?? []) {
|
|
1042
|
+
const key = productKey(p.type, p.code);
|
|
1043
|
+
if (key) cachedProducts.set(key, p);
|
|
1044
|
+
}
|
|
1045
|
+
const mergedProducts = (raw.products ?? []).map((p) => {
|
|
1046
|
+
const key = productKey(p.type, p.code);
|
|
1047
|
+
const fallback = key ? cachedProducts.get(key) : void 0;
|
|
1048
|
+
return {
|
|
1049
|
+
...p,
|
|
1050
|
+
name: p.name ?? fallback?.name ?? null,
|
|
1051
|
+
totalAmount: p.totalAmount ?? fallback?.totalAmount,
|
|
1052
|
+
overrideAmount: p.overrideAmount ?? fallback?.overrideAmount,
|
|
1053
|
+
currency: p.currency ?? fallback?.currency
|
|
1054
|
+
};
|
|
1055
|
+
});
|
|
1019
1056
|
return {
|
|
1020
1057
|
...raw,
|
|
1021
1058
|
currency: raw.currency ?? cached?.currency,
|
|
1022
|
-
|
|
1023
|
-
const key = item.code ?? item.providerItemId;
|
|
1024
|
-
const fallback = key ? cachedItems.get(key) : void 0;
|
|
1025
|
-
const resolvedName = item.itemName ?? item.providerItemName ?? fallback?.itemName ?? fallback?.providerItemName;
|
|
1026
|
-
return {
|
|
1027
|
-
...item,
|
|
1028
|
-
itemName: resolvedName,
|
|
1029
|
-
providerItemName: resolvedName,
|
|
1030
|
-
totalAmount: item.totalAmount ?? fallback?.totalAmount,
|
|
1031
|
-
overrideAmount: item.overrideAmount ?? fallback?.overrideAmount,
|
|
1032
|
-
currency: item.currency ?? fallback?.currency
|
|
1033
|
-
};
|
|
1034
|
-
}),
|
|
1035
|
-
subscriptions: raw.subscriptions.map((sub) => {
|
|
1036
|
-
const key = sub.code ?? sub.providerPlanId;
|
|
1037
|
-
const fallback = key ? cachedSubs.get(key) : void 0;
|
|
1038
|
-
const resolvedName = sub.subscriptionName ?? sub.providerPlanName ?? fallback?.subscriptionName ?? fallback?.providerPlanName;
|
|
1039
|
-
return {
|
|
1040
|
-
...sub,
|
|
1041
|
-
subscriptionName: resolvedName,
|
|
1042
|
-
providerPlanName: resolvedName,
|
|
1043
|
-
totalAmount: sub.totalAmount ?? fallback?.totalAmount,
|
|
1044
|
-
overrideAmount: sub.overrideAmount ?? fallback?.overrideAmount,
|
|
1045
|
-
currency: sub.currency ?? fallback?.currency
|
|
1046
|
-
};
|
|
1047
|
-
})
|
|
1059
|
+
products: mergedProducts
|
|
1048
1060
|
};
|
|
1049
1061
|
}
|
|
1050
1062
|
};
|
|
@@ -1181,12 +1193,33 @@ async function loadFloPay(publishableKey, options) {
|
|
|
1181
1193
|
|
|
1182
1194
|
// src/create-checkout-session.ts
|
|
1183
1195
|
var import_shared6 = require("@flopay/shared");
|
|
1196
|
+
var MAX_COUPON_CODES = 5;
|
|
1197
|
+
function readString2(value) {
|
|
1198
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1199
|
+
}
|
|
1200
|
+
function buildCheckoutSessionError(status, payload) {
|
|
1201
|
+
const nested = payload?.error;
|
|
1202
|
+
const code = readString2(payload?.code) ?? readString2(nested?.code) ?? `http_${status}`;
|
|
1203
|
+
const message = readString2(payload?.message) ?? readString2(nested?.message) ?? defaultMessageForCode(code, status);
|
|
1204
|
+
return new import_shared6.FloPayError(message, "api_error", { code, statusCode: status });
|
|
1205
|
+
}
|
|
1206
|
+
function defaultMessageForCode(code, status) {
|
|
1207
|
+
switch (code) {
|
|
1208
|
+
case "CouponLimitExceeded":
|
|
1209
|
+
return `Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`;
|
|
1210
|
+
case "CouponCurrencyUnsupported":
|
|
1211
|
+
return "One of the applied coupons has no price configured for the cart currency.";
|
|
1212
|
+
default:
|
|
1213
|
+
return `Failed to create checkout session (HTTP ${status}).`;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1184
1216
|
async function createCheckoutSession(options) {
|
|
1185
1217
|
const {
|
|
1186
1218
|
billingApiUrl,
|
|
1187
1219
|
checkoutBaseUrl,
|
|
1188
1220
|
items = [],
|
|
1189
1221
|
subscriptions = [],
|
|
1222
|
+
products,
|
|
1190
1223
|
account,
|
|
1191
1224
|
successUrl,
|
|
1192
1225
|
cancelUrl,
|
|
@@ -1200,15 +1233,30 @@ async function createCheckoutSession(options) {
|
|
|
1200
1233
|
currency,
|
|
1201
1234
|
utmMetadata
|
|
1202
1235
|
} = options;
|
|
1203
|
-
|
|
1236
|
+
if (couponCodes.length > MAX_COUPON_CODES) {
|
|
1237
|
+
throw new import_shared6.FloPayError(
|
|
1238
|
+
`Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
|
|
1239
|
+
"validation_error",
|
|
1240
|
+
{ code: "CouponLimitExceeded", param: "couponCodes" }
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const wireProducts = products ?? (0, import_shared6.foldIntoProducts)(items, subscriptions);
|
|
1244
|
+
const sessionCurrency = (0, import_shared6.resolveSessionCurrency)(currency, items, subscriptions, wireProducts);
|
|
1245
|
+
if (!sessionCurrency) {
|
|
1246
|
+
throw new import_shared6.FloPayError(
|
|
1247
|
+
"currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
|
|
1248
|
+
"validation_error",
|
|
1249
|
+
{ code: "CurrencyRequired", param: "currency" }
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1204
1252
|
const payload = {
|
|
1205
1253
|
clientId,
|
|
1254
|
+
checkoutVersion: import_shared6.SDK_VERSION,
|
|
1206
1255
|
successUrl,
|
|
1207
1256
|
cancelUrl,
|
|
1208
1257
|
currency: sessionCurrency,
|
|
1209
1258
|
checkoutMode,
|
|
1210
|
-
|
|
1211
|
-
subscriptions: subscriptions.map((sub) => (0, import_shared6.buildSubscriptionPayload)(sub, sessionCurrency)),
|
|
1259
|
+
products: wireProducts.map((product) => (0, import_shared6.buildProductPayload)(product, sessionCurrency)),
|
|
1212
1260
|
accountData: {
|
|
1213
1261
|
userId: account.userId,
|
|
1214
1262
|
firstName: account.firstName ?? null,
|
|
@@ -1250,16 +1298,25 @@ async function createCheckoutSession(options) {
|
|
|
1250
1298
|
} finally {
|
|
1251
1299
|
clearTimeout(timer);
|
|
1252
1300
|
}
|
|
1301
|
+
if (status >= 400) {
|
|
1302
|
+
throw buildCheckoutSessionError(status, body);
|
|
1303
|
+
}
|
|
1253
1304
|
if (status === 201) {
|
|
1254
1305
|
const uuid = body?.data?.uuid;
|
|
1255
1306
|
if (!uuid) {
|
|
1256
1307
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1257
1308
|
}
|
|
1258
|
-
if (
|
|
1309
|
+
if (wireProducts.length || sessionCurrency) {
|
|
1259
1310
|
cacheSessionDisplayData(uuid, {
|
|
1260
|
-
currency,
|
|
1261
|
-
|
|
1262
|
-
|
|
1311
|
+
currency: sessionCurrency,
|
|
1312
|
+
products: wireProducts.map((p) => ({
|
|
1313
|
+
code: p.code ?? p.providerItemId ?? p.providerPlanId,
|
|
1314
|
+
type: p.type,
|
|
1315
|
+
name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
|
|
1316
|
+
totalAmount: p.totalAmount,
|
|
1317
|
+
overrideAmount: p.overrideAmount,
|
|
1318
|
+
currency: p.currency ?? sessionCurrency
|
|
1319
|
+
}))
|
|
1263
1320
|
});
|
|
1264
1321
|
}
|
|
1265
1322
|
const redirectUrl = new URL(`${checkoutBaseUrl.replace(/\/+$/, "")}/secure`);
|