@solvapay/react 1.0.8-preview.7 → 1.0.8-preview.9
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 +2 -6
- package/dist/index.js +2 -6
- package/package.json +2 -2
- package/dist/CancelPlanButton-BKH_yv1d.d.ts +0 -1113
- package/dist/CancelPlanButton-Bb7cvym9.d.cts +0 -1105
- package/dist/CancelPlanButton-Cq117t7h.d.ts +0 -1105
- package/dist/CancelPlanButton-DQXUTHmF.d.cts +0 -1113
- package/dist/PaymentForm-B5dw0i1X.d.ts +0 -934
- package/dist/PaymentForm-CesoRQWu.d.cts +0 -934
- package/dist/chunk-5FZBOPIW.js +0 -4763
- package/dist/chunk-C7SKOENW.js +0 -1889
- package/dist/chunk-ISYAH4ZL.js +0 -1889
- package/dist/chunk-Q7GPX6X4.js +0 -4748
- package/dist/chunk-TP4JPKE2.js +0 -3126
- package/dist/chunk-VTGIXJQU.js +0 -4755
- package/dist/chunk-XMQ4GBWQ.js +0 -4752
- package/dist/index-DLl3UwO3.d.ts +0 -798
- package/dist/index-Sj2lHLl8.d.cts +0 -798
- package/dist/src/PaymentForm.d.ts +0 -56
- package/dist/src/PaymentForm.d.ts.map +0 -1
- package/dist/src/PaymentForm.js +0 -212
- package/dist/src/PaymentForm.js.map +0 -1
- package/dist/src/SolvaPayProvider.d.ts +0 -84
- package/dist/src/SolvaPayProvider.d.ts.map +0 -1
- package/dist/src/SolvaPayProvider.js +0 -537
- package/dist/src/SolvaPayProvider.js.map +0 -1
- package/dist/src/adapters/auth.d.ts +0 -43
- package/dist/src/adapters/auth.d.ts.map +0 -1
- package/dist/src/adapters/auth.js +0 -38
- package/dist/src/adapters/auth.js.map +0 -1
- package/dist/src/components/PlanBadge.d.ts +0 -31
- package/dist/src/components/PlanBadge.d.ts.map +0 -1
- package/dist/src/components/PlanBadge.js +0 -119
- package/dist/src/components/PlanBadge.js.map +0 -1
- package/dist/src/components/PlanSelector.d.ts +0 -49
- package/dist/src/components/PlanSelector.d.ts.map +0 -1
- package/dist/src/components/PlanSelector.js +0 -82
- package/dist/src/components/PlanSelector.js.map +0 -1
- package/dist/src/components/Spinner.d.ts +0 -10
- package/dist/src/components/Spinner.d.ts.map +0 -1
- package/dist/src/components/Spinner.js +0 -14
- package/dist/src/components/Spinner.js.map +0 -1
- package/dist/src/components/StripePaymentFormWrapper.d.ts +0 -17
- package/dist/src/components/StripePaymentFormWrapper.d.ts.map +0 -1
- package/dist/src/components/StripePaymentFormWrapper.js +0 -158
- package/dist/src/components/StripePaymentFormWrapper.js.map +0 -1
- package/dist/src/components/SubscriptionGate.d.ts +0 -21
- package/dist/src/components/SubscriptionGate.d.ts.map +0 -1
- package/dist/src/components/SubscriptionGate.js +0 -32
- package/dist/src/components/SubscriptionGate.js.map +0 -1
- package/dist/src/hooks/__tests__/useSubscription.test.d.ts +0 -2
- package/dist/src/hooks/__tests__/useSubscription.test.d.ts.map +0 -1
- package/dist/src/hooks/__tests__/useSubscription.test.js +0 -554
- package/dist/src/hooks/__tests__/useSubscription.test.js.map +0 -1
- package/dist/src/hooks/__tests__/useSubscriptionStatus.test.d.ts +0 -2
- package/dist/src/hooks/__tests__/useSubscriptionStatus.test.d.ts.map +0 -1
- package/dist/src/hooks/__tests__/useSubscriptionStatus.test.js +0 -719
- package/dist/src/hooks/__tests__/useSubscriptionStatus.test.js.map +0 -1
- package/dist/src/hooks/useCheckout.d.ts +0 -59
- package/dist/src/hooks/useCheckout.d.ts.map +0 -1
- package/dist/src/hooks/useCheckout.js +0 -138
- package/dist/src/hooks/useCheckout.js.map +0 -1
- package/dist/src/hooks/useCustomer.d.ts +0 -43
- package/dist/src/hooks/useCustomer.d.ts.map +0 -1
- package/dist/src/hooks/useCustomer.js +0 -31
- package/dist/src/hooks/useCustomer.js.map +0 -1
- package/dist/src/hooks/usePlans.d.ts +0 -28
- package/dist/src/hooks/usePlans.d.ts.map +0 -1
- package/dist/src/hooks/usePlans.js +0 -181
- package/dist/src/hooks/usePlans.js.map +0 -1
- package/dist/src/hooks/useSolvaPay.d.ts +0 -44
- package/dist/src/hooks/useSolvaPay.d.ts.map +0 -1
- package/dist/src/hooks/useSolvaPay.js +0 -52
- package/dist/src/hooks/useSolvaPay.js.map +0 -1
- package/dist/src/hooks/useSubscription.d.ts +0 -46
- package/dist/src/hooks/useSubscription.d.ts.map +0 -1
- package/dist/src/hooks/useSubscription.js +0 -50
- package/dist/src/hooks/useSubscription.js.map +0 -1
- package/dist/src/hooks/useSubscriptionStatus.d.ts +0 -19
- package/dist/src/hooks/useSubscriptionStatus.d.ts.map +0 -1
- package/dist/src/hooks/useSubscriptionStatus.js +0 -68
- package/dist/src/hooks/useSubscriptionStatus.js.map +0 -1
- package/dist/src/index.d.ts +0 -24
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -26
- package/dist/src/index.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -339
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -5
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/utils/__tests__/subscriptions.test.d.ts +0 -2
- package/dist/src/utils/__tests__/subscriptions.test.d.ts.map +0 -1
- package/dist/src/utils/__tests__/subscriptions.test.js +0 -121
- package/dist/src/utils/__tests__/subscriptions.test.js.map +0 -1
- package/dist/src/utils/subscriptions.d.ts +0 -57
- package/dist/src/utils/subscriptions.d.ts.map +0 -1
- package/dist/src/utils/subscriptions.js +0 -85
- package/dist/src/utils/subscriptions.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/chunk-ISYAH4ZL.js
DELETED
|
@@ -1,1889 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defaultAuthAdapter
|
|
3
|
-
} from "./chunk-OUSEQRCT.js";
|
|
4
|
-
|
|
5
|
-
// src/utils/purchases.ts
|
|
6
|
-
function filterPurchases(purchases) {
|
|
7
|
-
return purchases.filter((purchase) => purchase.status === "active");
|
|
8
|
-
}
|
|
9
|
-
function getActivePurchases(purchases) {
|
|
10
|
-
return purchases.filter((purchase) => purchase.status === "active");
|
|
11
|
-
}
|
|
12
|
-
function getCancelledPurchasesWithEndDate(purchases) {
|
|
13
|
-
const now = /* @__PURE__ */ new Date();
|
|
14
|
-
return purchases.filter((purchase) => {
|
|
15
|
-
return purchase.status === "active" && purchase.cancelledAt && purchase.endDate && new Date(purchase.endDate) > now;
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
function getMostRecentPurchase(purchases) {
|
|
19
|
-
if (purchases.length === 0) return null;
|
|
20
|
-
return purchases.reduce((latest, current) => {
|
|
21
|
-
return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
function getPrimaryPurchase(purchases) {
|
|
25
|
-
const filtered = filterPurchases(purchases);
|
|
26
|
-
if (filtered.length > 0) {
|
|
27
|
-
return getMostRecentPurchase(filtered);
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
function isPaidPurchase(purchase) {
|
|
32
|
-
return (purchase.amount ?? 0) > 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/i18n/en.ts
|
|
36
|
-
function intervalPhrase(ctx) {
|
|
37
|
-
const interval = ctx.plan?.interval;
|
|
38
|
-
const count = ctx.plan?.intervalCount ?? 1;
|
|
39
|
-
if (!interval) return "";
|
|
40
|
-
return count > 1 ? `${count} ${interval}s` : interval;
|
|
41
|
-
}
|
|
42
|
-
function trialPhrase(ctx) {
|
|
43
|
-
const trialDays = ctx.plan?.trialDays ?? ctx.trialDays;
|
|
44
|
-
return trialDays ? ` after your ${trialDays}-day free trial` : "";
|
|
45
|
-
}
|
|
46
|
-
function termsSentence(ctx) {
|
|
47
|
-
const { termsUrl, privacyUrl } = ctx.merchant;
|
|
48
|
-
if (termsUrl && privacyUrl) {
|
|
49
|
-
return ` See ${termsUrl} and ${privacyUrl}.`;
|
|
50
|
-
}
|
|
51
|
-
if (termsUrl) return ` See ${termsUrl}.`;
|
|
52
|
-
if (privacyUrl) return ` See ${privacyUrl}.`;
|
|
53
|
-
return "";
|
|
54
|
-
}
|
|
55
|
-
var enCopy = {
|
|
56
|
-
mandate: {
|
|
57
|
-
recurring: (ctx) => {
|
|
58
|
-
const period = intervalPhrase(ctx);
|
|
59
|
-
const trial = trialPhrase(ctx);
|
|
60
|
-
const every = period ? ` every ${period}` : "";
|
|
61
|
-
return `By subscribing, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${every}${trial} until you cancel. You can cancel any time. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
62
|
-
},
|
|
63
|
-
oneTime: (ctx) => {
|
|
64
|
-
const product = ctx.product?.name ? ` for ${ctx.product.name}` : "";
|
|
65
|
-
return `By confirming, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${product}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
66
|
-
},
|
|
67
|
-
topup: (ctx) => {
|
|
68
|
-
const product = ctx.product?.name ? ` to add credits to your ${ctx.product.name} balance` : " to add credits to your balance";
|
|
69
|
-
return `By confirming, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${product}. Credits are non-refundable once used. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
70
|
-
},
|
|
71
|
-
usageMetered: (ctx) => {
|
|
72
|
-
const measures = ctx.plan?.measures ?? "unit";
|
|
73
|
-
const cycle = ctx.plan?.billingCycle ?? "monthly";
|
|
74
|
-
const product = ctx.product?.name ?? "the service";
|
|
75
|
-
return `By confirming, you authorize ${ctx.merchant.legalName} to charge your payment method for metered usage of ${product} at ${ctx.amountFormatted} per ${measures}, billed ${cycle}. You can cancel any time. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
76
|
-
},
|
|
77
|
-
freeTier: (ctx) => {
|
|
78
|
-
const product = ctx.product?.name ?? "this plan";
|
|
79
|
-
const planPhrase = ctx.plan?.name ? ` on ${ctx.plan.name}` : "";
|
|
80
|
-
return `By confirming, you activate ${product}${planPhrase}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
cta: {
|
|
84
|
-
payNow: "Pay Now",
|
|
85
|
-
topUp: "Top Up",
|
|
86
|
-
subscribe: "Subscribe",
|
|
87
|
-
trialStart: "Start {trialDays}-day free trial",
|
|
88
|
-
payAmount: "Pay {amount}",
|
|
89
|
-
addAmount: "Add {amount}",
|
|
90
|
-
startUsing: "Start using {product}",
|
|
91
|
-
processing: "Processing..."
|
|
92
|
-
},
|
|
93
|
-
interval: {
|
|
94
|
-
day: "day",
|
|
95
|
-
week: "week",
|
|
96
|
-
month: "month",
|
|
97
|
-
year: "year",
|
|
98
|
-
every: "every {n} {unit}",
|
|
99
|
-
free: "Free",
|
|
100
|
-
trial: "{trialDays}-day free trial"
|
|
101
|
-
},
|
|
102
|
-
terms: {
|
|
103
|
-
checkboxLabel: "I agree to the terms and privacy policy"
|
|
104
|
-
},
|
|
105
|
-
customer: {
|
|
106
|
-
chargingTo: "Charging to {email}",
|
|
107
|
-
emailLabel: "Email",
|
|
108
|
-
nameLabel: "Name"
|
|
109
|
-
},
|
|
110
|
-
balance: {
|
|
111
|
-
credits: " credits",
|
|
112
|
-
currencyEquivalent: " (~{amount})"
|
|
113
|
-
},
|
|
114
|
-
product: {
|
|
115
|
-
currentProductLabel: "Current product: {name}"
|
|
116
|
-
},
|
|
117
|
-
topup: {
|
|
118
|
-
selectOrEnterAmount: "Please select or enter an amount",
|
|
119
|
-
minAmount: "Minimum amount is {amount}",
|
|
120
|
-
maxAmount: "Maximum amount is {amount}"
|
|
121
|
-
},
|
|
122
|
-
activation: {
|
|
123
|
-
paymentRequired: "This plan requires payment. Please select a different plan.",
|
|
124
|
-
invalidConfiguration: "Invalid plan configuration.",
|
|
125
|
-
unexpectedResponse: "Unexpected response from server.",
|
|
126
|
-
failed: "Activation failed"
|
|
127
|
-
},
|
|
128
|
-
planSelector: {
|
|
129
|
-
heading: "Choose your pricing",
|
|
130
|
-
currentBadge: "Current",
|
|
131
|
-
popularBadge: "Popular",
|
|
132
|
-
freeBadge: "Free",
|
|
133
|
-
perIntervalShort: "/{interval}",
|
|
134
|
-
continueButton: "Continue",
|
|
135
|
-
backButton: "\u2190 Back to plans",
|
|
136
|
-
trialBadge: "{trialDays}-day free trial"
|
|
137
|
-
},
|
|
138
|
-
amountPicker: {
|
|
139
|
-
selectAmountLabel: "Select an amount",
|
|
140
|
-
customAmountLabel: "Or enter a custom amount",
|
|
141
|
-
creditEstimateExact: "= {credits} credits",
|
|
142
|
-
creditEstimateApprox: "~ {credits} credits"
|
|
143
|
-
},
|
|
144
|
-
activationFlow: {
|
|
145
|
-
heading: "Confirm your plan",
|
|
146
|
-
activateButton: "Activate",
|
|
147
|
-
activatingLabel: "Activating...",
|
|
148
|
-
topupHeading: "Add credits",
|
|
149
|
-
topupSubheading: "Top up your credits to activate this plan.",
|
|
150
|
-
continueToPayment: "Continue to payment",
|
|
151
|
-
changeAmountButton: "Change amount",
|
|
152
|
-
retryingHeading: "Activating your plan...",
|
|
153
|
-
retryingSubheading: "Payment received. Setting up your plan.",
|
|
154
|
-
activatedHeading: "Plan selected",
|
|
155
|
-
activatedSubheading: "Your plan is now active.",
|
|
156
|
-
tryAgainButton: "Try Again",
|
|
157
|
-
backButton: "\u2190 Back to plan selection"
|
|
158
|
-
},
|
|
159
|
-
cancelPlan: {
|
|
160
|
-
button: "Cancel plan",
|
|
161
|
-
buttonLoading: "Cancelling...",
|
|
162
|
-
confirmRecurring: "Are you sure you want to cancel your subscription?",
|
|
163
|
-
confirmUsageBased: "Are you sure you want to deactivate your plan? This will take effect immediately."
|
|
164
|
-
},
|
|
165
|
-
cancelledNotice: {
|
|
166
|
-
heading: "Your purchase has been cancelled",
|
|
167
|
-
expiresLabel: "Purchase Expires: {date}",
|
|
168
|
-
daysRemaining: "{days} days remaining",
|
|
169
|
-
dayRemaining: "1 day remaining",
|
|
170
|
-
accessUntil: "You'll continue to have access to {product} features until this date",
|
|
171
|
-
accessEnded: "Your purchase access has ended",
|
|
172
|
-
cancelledOn: "Cancelled on {date}",
|
|
173
|
-
reactivateButton: "Undo Cancellation",
|
|
174
|
-
reactivateButtonLoading: "Reactivating..."
|
|
175
|
-
},
|
|
176
|
-
creditGate: {
|
|
177
|
-
lowBalanceHeading: "You're out of credits",
|
|
178
|
-
lowBalanceSubheading: "Top up to continue using {product}",
|
|
179
|
-
topUpCta: "Top up now"
|
|
180
|
-
},
|
|
181
|
-
errors: {
|
|
182
|
-
paymentInitFailed: "Payment initialization failed",
|
|
183
|
-
topupInitFailed: "Top-up initialization failed",
|
|
184
|
-
configMissingPlanOrProduct: "PaymentForm: either planRef or productRef is required",
|
|
185
|
-
configMissingAmount: "TopupForm: amount must be a positive number",
|
|
186
|
-
unknownError: "Unknown error",
|
|
187
|
-
stripeUnavailable: "Stripe is not available. Please refresh the page.",
|
|
188
|
-
paymentIntentUnavailable: "Payment intent not available. Please refresh the page.",
|
|
189
|
-
cardElementMissing: "Card element not found",
|
|
190
|
-
paymentUnexpected: "An unexpected error occurred.",
|
|
191
|
-
paymentProcessingFailed: "Payment processing failed. Please try again or contact support.",
|
|
192
|
-
paymentRequires3ds: "Payment requires additional authentication. Please complete the verification.",
|
|
193
|
-
paymentProcessingTimeout: "Payment processing timed out \u2014 webhooks may not be configured",
|
|
194
|
-
paymentStatusPrefix: "Payment status: {status}"
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// src/i18n/merge.ts
|
|
199
|
-
function mergeCopy(defaults, overrides) {
|
|
200
|
-
if (!overrides) return defaults;
|
|
201
|
-
const merged = { ...defaults };
|
|
202
|
-
for (const sectionKey of Object.keys(overrides)) {
|
|
203
|
-
const defaultSection = defaults[sectionKey];
|
|
204
|
-
const overrideSection = overrides[sectionKey];
|
|
205
|
-
if (!overrideSection) continue;
|
|
206
|
-
const combined = { ...defaultSection, ...overrideSection };
|
|
207
|
-
merged[sectionKey] = combined;
|
|
208
|
-
}
|
|
209
|
-
return merged;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// src/i18n/context.tsx
|
|
213
|
-
import { createContext, useContext, useMemo } from "react";
|
|
214
|
-
import { jsx } from "react/jsx-runtime";
|
|
215
|
-
var CopyContext = createContext({
|
|
216
|
-
locale: void 0,
|
|
217
|
-
copy: enCopy
|
|
218
|
-
});
|
|
219
|
-
var CopyProvider = ({
|
|
220
|
-
locale,
|
|
221
|
-
copy: overrides,
|
|
222
|
-
children
|
|
223
|
-
}) => {
|
|
224
|
-
const value = useMemo(
|
|
225
|
-
() => ({
|
|
226
|
-
locale,
|
|
227
|
-
copy: mergeCopy(enCopy, overrides)
|
|
228
|
-
}),
|
|
229
|
-
[locale, overrides]
|
|
230
|
-
);
|
|
231
|
-
return /* @__PURE__ */ jsx(CopyContext.Provider, { value, children });
|
|
232
|
-
};
|
|
233
|
-
function useCopyContext() {
|
|
234
|
-
return useContext(CopyContext);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// src/SolvaPayProvider.tsx
|
|
238
|
-
import { createContext as createContext2, useState, useEffect, useCallback, useMemo as useMemo2, useRef } from "react";
|
|
239
|
-
|
|
240
|
-
// src/utils/headers.ts
|
|
241
|
-
var CUSTOMER_REF_KEY = "solvapay_customerRef";
|
|
242
|
-
var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
|
|
243
|
-
var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
|
|
244
|
-
function getAuthAdapter(config) {
|
|
245
|
-
if (config?.auth?.adapter) {
|
|
246
|
-
return config.auth.adapter;
|
|
247
|
-
}
|
|
248
|
-
if (config?.auth?.getToken || config?.auth?.getUserId) {
|
|
249
|
-
return {
|
|
250
|
-
async getToken() {
|
|
251
|
-
return config?.auth?.getToken?.() || null;
|
|
252
|
-
},
|
|
253
|
-
async getUserId() {
|
|
254
|
-
return config?.auth?.getUserId?.() || null;
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return defaultAuthAdapter;
|
|
259
|
-
}
|
|
260
|
-
function getCachedCustomerRef(userId) {
|
|
261
|
-
if (typeof window === "undefined") return null;
|
|
262
|
-
const cached = localStorage.getItem(CUSTOMER_REF_KEY);
|
|
263
|
-
const expiry = localStorage.getItem(CUSTOMER_REF_EXPIRY);
|
|
264
|
-
const cachedUserId = localStorage.getItem(CUSTOMER_REF_USER_ID_KEY);
|
|
265
|
-
if (!cached || !expiry) return null;
|
|
266
|
-
if (Date.now() > parseInt(expiry)) {
|
|
267
|
-
localStorage.removeItem(CUSTOMER_REF_KEY);
|
|
268
|
-
localStorage.removeItem(CUSTOMER_REF_EXPIRY);
|
|
269
|
-
localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
if (userId !== void 0 && userId !== null) {
|
|
273
|
-
if (cachedUserId !== userId) {
|
|
274
|
-
clearCachedCustomerRef();
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return cached;
|
|
279
|
-
}
|
|
280
|
-
var CACHE_DURATION = 24 * 60 * 60 * 1e3;
|
|
281
|
-
function setCachedCustomerRef(customerRef, userId) {
|
|
282
|
-
if (typeof window === "undefined") return;
|
|
283
|
-
if (userId === void 0 || userId === null) {
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
localStorage.setItem(CUSTOMER_REF_KEY, customerRef);
|
|
287
|
-
localStorage.setItem(CUSTOMER_REF_EXPIRY, String(Date.now() + CACHE_DURATION));
|
|
288
|
-
localStorage.setItem(CUSTOMER_REF_USER_ID_KEY, userId);
|
|
289
|
-
}
|
|
290
|
-
function clearCachedCustomerRef() {
|
|
291
|
-
if (typeof window === "undefined") return;
|
|
292
|
-
localStorage.removeItem(CUSTOMER_REF_KEY);
|
|
293
|
-
localStorage.removeItem(CUSTOMER_REF_EXPIRY);
|
|
294
|
-
localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
|
|
295
|
-
}
|
|
296
|
-
async function buildRequestHeaders(config) {
|
|
297
|
-
const adapter = getAuthAdapter(config);
|
|
298
|
-
const token = await adapter.getToken();
|
|
299
|
-
const userId = await adapter.getUserId();
|
|
300
|
-
const cachedRef = getCachedCustomerRef(userId);
|
|
301
|
-
const headers = {
|
|
302
|
-
"Content-Type": "application/json"
|
|
303
|
-
};
|
|
304
|
-
if (token) {
|
|
305
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
306
|
-
}
|
|
307
|
-
if (cachedRef) {
|
|
308
|
-
headers["x-solvapay-customer-ref"] = cachedRef;
|
|
309
|
-
}
|
|
310
|
-
if (config?.headers) {
|
|
311
|
-
const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
|
|
312
|
-
Object.assign(headers, custom);
|
|
313
|
-
}
|
|
314
|
-
return { headers, userId };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// src/SolvaPayProvider.tsx
|
|
318
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
319
|
-
var SolvaPayContext = createContext2(null);
|
|
320
|
-
var SolvaPayProvider = ({
|
|
321
|
-
config,
|
|
322
|
-
createPayment: customCreatePayment,
|
|
323
|
-
checkPurchase: customCheckPurchase,
|
|
324
|
-
processPayment: customProcessPayment,
|
|
325
|
-
createTopupPayment: customCreateTopupPayment,
|
|
326
|
-
children
|
|
327
|
-
}) => {
|
|
328
|
-
const [purchaseData, setPurchaseData] = useState({
|
|
329
|
-
purchases: []
|
|
330
|
-
});
|
|
331
|
-
const [loading, setLoading] = useState(false);
|
|
332
|
-
const [isRefetching, setIsRefetching] = useState(false);
|
|
333
|
-
const [purchaseError, setPurchaseError] = useState(null);
|
|
334
|
-
const [internalCustomerRef, setInternalCustomerRef] = useState(void 0);
|
|
335
|
-
const [userId, setUserId] = useState(null);
|
|
336
|
-
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
337
|
-
const [creditsValue, setCreditsValue] = useState(null);
|
|
338
|
-
const [displayCurrencyValue, setDisplayCurrencyValue] = useState(null);
|
|
339
|
-
const [creditsPerMinorUnitValue, setCreditsPerMinorUnitValue] = useState(null);
|
|
340
|
-
const [displayExchangeRateValue, setDisplayExchangeRateValue] = useState(null);
|
|
341
|
-
const [balanceLoading, setBalanceLoading] = useState(false);
|
|
342
|
-
const balanceInFlightRef = useRef(false);
|
|
343
|
-
const balanceLoadedRef = useRef(false);
|
|
344
|
-
const optimisticUntilRef = useRef(0);
|
|
345
|
-
const optimisticTimerRef = useRef(null);
|
|
346
|
-
const fetchBalanceRef = useRef(null);
|
|
347
|
-
const inFlightRef = useRef(null);
|
|
348
|
-
const checkPurchaseRef = useRef(null);
|
|
349
|
-
const createPaymentRef = useRef(null);
|
|
350
|
-
const processPaymentRef = useRef(null);
|
|
351
|
-
const createTopupPaymentRef = useRef(null);
|
|
352
|
-
const configRef = useRef(config);
|
|
353
|
-
const buildDefaultCheckPurchaseRef = useRef(
|
|
354
|
-
null
|
|
355
|
-
);
|
|
356
|
-
useEffect(() => {
|
|
357
|
-
configRef.current = config;
|
|
358
|
-
}, [config]);
|
|
359
|
-
useEffect(() => {
|
|
360
|
-
checkPurchaseRef.current = customCheckPurchase || null;
|
|
361
|
-
}, [customCheckPurchase]);
|
|
362
|
-
useEffect(() => {
|
|
363
|
-
createPaymentRef.current = customCreatePayment || null;
|
|
364
|
-
}, [customCreatePayment]);
|
|
365
|
-
useEffect(() => {
|
|
366
|
-
processPaymentRef.current = customProcessPayment || null;
|
|
367
|
-
}, [customProcessPayment]);
|
|
368
|
-
useEffect(() => {
|
|
369
|
-
createTopupPaymentRef.current = customCreateTopupPayment || null;
|
|
370
|
-
}, [customCreateTopupPayment]);
|
|
371
|
-
const buildDefaultCheckPurchase = useCallback(async () => {
|
|
372
|
-
const currentConfig = configRef.current;
|
|
373
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
374
|
-
const route = currentConfig?.api?.checkPurchase || "/api/check-purchase";
|
|
375
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
376
|
-
const res = await fetchFn(route, { method: "GET", headers });
|
|
377
|
-
if (!res.ok) {
|
|
378
|
-
const error = new Error(`Failed to check purchase: ${res.statusText}`);
|
|
379
|
-
currentConfig?.onError?.(error, "checkPurchase");
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
return res.json();
|
|
383
|
-
}, []);
|
|
384
|
-
useEffect(() => {
|
|
385
|
-
buildDefaultCheckPurchaseRef.current = buildDefaultCheckPurchase;
|
|
386
|
-
}, [buildDefaultCheckPurchase]);
|
|
387
|
-
const buildDefaultCreatePayment = useCallback(
|
|
388
|
-
async (params) => {
|
|
389
|
-
const currentConfig = configRef.current;
|
|
390
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
391
|
-
const route = currentConfig?.api?.createPayment || "/api/create-payment-intent";
|
|
392
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
393
|
-
const body = {};
|
|
394
|
-
if (params.planRef) {
|
|
395
|
-
body.planRef = params.planRef;
|
|
396
|
-
}
|
|
397
|
-
if (params.productRef) {
|
|
398
|
-
body.productRef = params.productRef;
|
|
399
|
-
}
|
|
400
|
-
if (params.customer && (params.customer.name || params.customer.email)) {
|
|
401
|
-
body.customer = params.customer;
|
|
402
|
-
}
|
|
403
|
-
const res = await fetchFn(route, {
|
|
404
|
-
method: "POST",
|
|
405
|
-
headers,
|
|
406
|
-
body: JSON.stringify(body)
|
|
407
|
-
});
|
|
408
|
-
if (!res.ok) {
|
|
409
|
-
const error = new Error(`Failed to create payment: ${res.statusText}`);
|
|
410
|
-
currentConfig?.onError?.(error, "createPayment");
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
return res.json();
|
|
414
|
-
},
|
|
415
|
-
[]
|
|
416
|
-
);
|
|
417
|
-
const buildDefaultProcessPayment = useCallback(
|
|
418
|
-
async (params) => {
|
|
419
|
-
const currentConfig = configRef.current;
|
|
420
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
421
|
-
const route = currentConfig?.api?.processPayment || "/api/process-payment";
|
|
422
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
423
|
-
const res = await fetchFn(route, {
|
|
424
|
-
method: "POST",
|
|
425
|
-
headers,
|
|
426
|
-
body: JSON.stringify(params)
|
|
427
|
-
});
|
|
428
|
-
if (!res.ok) {
|
|
429
|
-
const error = new Error(`Failed to process payment: ${res.statusText}`);
|
|
430
|
-
currentConfig?.onError?.(error, "processPayment");
|
|
431
|
-
throw error;
|
|
432
|
-
}
|
|
433
|
-
return res.json();
|
|
434
|
-
},
|
|
435
|
-
[]
|
|
436
|
-
);
|
|
437
|
-
const buildDefaultCreateTopupPayment = useCallback(
|
|
438
|
-
async (params) => {
|
|
439
|
-
const currentConfig = configRef.current;
|
|
440
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
441
|
-
const route = currentConfig?.api?.createTopupPayment || "/api/create-topup-payment-intent";
|
|
442
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
443
|
-
const res = await fetchFn(route, {
|
|
444
|
-
method: "POST",
|
|
445
|
-
headers,
|
|
446
|
-
body: JSON.stringify({
|
|
447
|
-
amount: params.amount,
|
|
448
|
-
currency: params.currency
|
|
449
|
-
})
|
|
450
|
-
});
|
|
451
|
-
if (!res.ok) {
|
|
452
|
-
const error = new Error(`Failed to create topup payment: ${res.statusText}`);
|
|
453
|
-
currentConfig?.onError?.(error, "createTopupPayment");
|
|
454
|
-
throw error;
|
|
455
|
-
}
|
|
456
|
-
return res.json();
|
|
457
|
-
},
|
|
458
|
-
[]
|
|
459
|
-
);
|
|
460
|
-
const fetchBalanceImpl = useCallback(async () => {
|
|
461
|
-
if (optimisticUntilRef.current > Date.now()) return;
|
|
462
|
-
if (!isAuthenticated && !internalCustomerRef) {
|
|
463
|
-
setCreditsValue(null);
|
|
464
|
-
setDisplayCurrencyValue(null);
|
|
465
|
-
setCreditsPerMinorUnitValue(null);
|
|
466
|
-
setDisplayExchangeRateValue(null);
|
|
467
|
-
setBalanceLoading(false);
|
|
468
|
-
balanceLoadedRef.current = false;
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (balanceInFlightRef.current) return;
|
|
472
|
-
balanceInFlightRef.current = true;
|
|
473
|
-
if (!balanceLoadedRef.current) {
|
|
474
|
-
setBalanceLoading(true);
|
|
475
|
-
}
|
|
476
|
-
try {
|
|
477
|
-
const currentConfig = configRef.current;
|
|
478
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
479
|
-
const route = currentConfig?.api?.customerBalance || "/api/customer-balance";
|
|
480
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
481
|
-
const res = await fetchFn(route, { method: "GET", headers });
|
|
482
|
-
if (!res.ok) {
|
|
483
|
-
console.error("[SolvaPayProvider] Failed to fetch balance:", res.statusText);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const data = await res.json();
|
|
487
|
-
setCreditsValue(data.credits ?? null);
|
|
488
|
-
setDisplayCurrencyValue(data.displayCurrency ?? null);
|
|
489
|
-
setCreditsPerMinorUnitValue(data.creditsPerMinorUnit ?? null);
|
|
490
|
-
setDisplayExchangeRateValue(data.displayExchangeRate ?? null);
|
|
491
|
-
balanceLoadedRef.current = true;
|
|
492
|
-
} catch (error) {
|
|
493
|
-
console.error("[SolvaPayProvider] Failed to fetch balance:", error);
|
|
494
|
-
} finally {
|
|
495
|
-
setBalanceLoading(false);
|
|
496
|
-
balanceInFlightRef.current = false;
|
|
497
|
-
}
|
|
498
|
-
}, [isAuthenticated, internalCustomerRef]);
|
|
499
|
-
useEffect(() => {
|
|
500
|
-
fetchBalanceRef.current = fetchBalanceImpl;
|
|
501
|
-
}, [fetchBalanceImpl]);
|
|
502
|
-
useEffect(() => {
|
|
503
|
-
return () => {
|
|
504
|
-
if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
|
|
505
|
-
};
|
|
506
|
-
}, []);
|
|
507
|
-
const OPTIMISTIC_GRACE_MS = 8e3;
|
|
508
|
-
const adjustBalanceImpl = useCallback((credits) => {
|
|
509
|
-
setCreditsValue((prev) => (prev ?? 0) + credits);
|
|
510
|
-
optimisticUntilRef.current = Date.now() + OPTIMISTIC_GRACE_MS;
|
|
511
|
-
if (optimisticTimerRef.current) clearTimeout(optimisticTimerRef.current);
|
|
512
|
-
optimisticTimerRef.current = setTimeout(() => {
|
|
513
|
-
optimisticUntilRef.current = 0;
|
|
514
|
-
fetchBalanceRef.current?.();
|
|
515
|
-
}, OPTIMISTIC_GRACE_MS);
|
|
516
|
-
}, []);
|
|
517
|
-
const _checkPurchase = useCallback(async () => {
|
|
518
|
-
if (checkPurchaseRef.current) {
|
|
519
|
-
return checkPurchaseRef.current();
|
|
520
|
-
}
|
|
521
|
-
if (buildDefaultCheckPurchaseRef.current) {
|
|
522
|
-
return buildDefaultCheckPurchaseRef.current();
|
|
523
|
-
}
|
|
524
|
-
return buildDefaultCheckPurchase();
|
|
525
|
-
}, [buildDefaultCheckPurchase]);
|
|
526
|
-
const createPayment = useCallback(
|
|
527
|
-
async (params) => {
|
|
528
|
-
if (createPaymentRef.current) {
|
|
529
|
-
return createPaymentRef.current(params);
|
|
530
|
-
}
|
|
531
|
-
return buildDefaultCreatePayment(params);
|
|
532
|
-
},
|
|
533
|
-
[buildDefaultCreatePayment]
|
|
534
|
-
);
|
|
535
|
-
const processPayment = useCallback(
|
|
536
|
-
async (params) => {
|
|
537
|
-
if (processPaymentRef.current) {
|
|
538
|
-
return processPaymentRef.current(params);
|
|
539
|
-
}
|
|
540
|
-
return buildDefaultProcessPayment(params);
|
|
541
|
-
},
|
|
542
|
-
[buildDefaultProcessPayment]
|
|
543
|
-
);
|
|
544
|
-
const createTopupPayment = useCallback(
|
|
545
|
-
async (params) => {
|
|
546
|
-
if (createTopupPaymentRef.current) {
|
|
547
|
-
return createTopupPaymentRef.current(params);
|
|
548
|
-
}
|
|
549
|
-
return buildDefaultCreateTopupPayment(params);
|
|
550
|
-
},
|
|
551
|
-
[buildDefaultCreateTopupPayment]
|
|
552
|
-
);
|
|
553
|
-
useEffect(() => {
|
|
554
|
-
const detectAuth = async () => {
|
|
555
|
-
const currentConfig = configRef.current;
|
|
556
|
-
const adapter = getAuthAdapter(currentConfig);
|
|
557
|
-
const token = await adapter.getToken();
|
|
558
|
-
const detectedUserId = await adapter.getUserId();
|
|
559
|
-
const prevUserId = userId;
|
|
560
|
-
setIsAuthenticated(!!token);
|
|
561
|
-
setUserId(detectedUserId);
|
|
562
|
-
if (prevUserId !== null && detectedUserId !== prevUserId) {
|
|
563
|
-
clearCachedCustomerRef();
|
|
564
|
-
setInternalCustomerRef(void 0);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const cachedRef = getCachedCustomerRef(detectedUserId);
|
|
568
|
-
if (cachedRef && token) {
|
|
569
|
-
setInternalCustomerRef(cachedRef);
|
|
570
|
-
} else if (!token) {
|
|
571
|
-
clearCachedCustomerRef();
|
|
572
|
-
setInternalCustomerRef(void 0);
|
|
573
|
-
} else if (token && !cachedRef) {
|
|
574
|
-
setInternalCustomerRef(void 0);
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
detectAuth();
|
|
578
|
-
const interval = setInterval(detectAuth, 3e4);
|
|
579
|
-
return () => clearInterval(interval);
|
|
580
|
-
}, [userId]);
|
|
581
|
-
const fetchPurchase = useCallback(
|
|
582
|
-
async (force = false) => {
|
|
583
|
-
if (!isAuthenticated && !internalCustomerRef) {
|
|
584
|
-
setPurchaseData({ purchases: [] });
|
|
585
|
-
setPurchaseError(null);
|
|
586
|
-
setLoading(false);
|
|
587
|
-
setIsRefetching(false);
|
|
588
|
-
inFlightRef.current = null;
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
const cacheKey = internalCustomerRef || userId || "anonymous";
|
|
592
|
-
if (inFlightRef.current === cacheKey && !force) {
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
inFlightRef.current = cacheKey;
|
|
596
|
-
const hasExistingData = purchaseData.purchases.length > 0;
|
|
597
|
-
if (hasExistingData) {
|
|
598
|
-
setIsRefetching(true);
|
|
599
|
-
} else {
|
|
600
|
-
setLoading(true);
|
|
601
|
-
}
|
|
602
|
-
try {
|
|
603
|
-
const checkFn = checkPurchaseRef.current || buildDefaultCheckPurchaseRef.current;
|
|
604
|
-
if (!checkFn) {
|
|
605
|
-
throw new Error("checkPurchase function not available");
|
|
606
|
-
}
|
|
607
|
-
const data = await checkFn();
|
|
608
|
-
if (data.customerRef) {
|
|
609
|
-
setInternalCustomerRef(data.customerRef);
|
|
610
|
-
const currentAdapter = getAuthAdapter(configRef.current);
|
|
611
|
-
const currentUserId = await currentAdapter.getUserId();
|
|
612
|
-
setCachedCustomerRef(data.customerRef, currentUserId);
|
|
613
|
-
}
|
|
614
|
-
if (inFlightRef.current === cacheKey) {
|
|
615
|
-
const filteredData = {
|
|
616
|
-
...data,
|
|
617
|
-
purchases: filterPurchases(data.purchases || [])
|
|
618
|
-
};
|
|
619
|
-
setPurchaseData(filteredData);
|
|
620
|
-
setPurchaseError(null);
|
|
621
|
-
}
|
|
622
|
-
} catch (err) {
|
|
623
|
-
console.error("[SolvaPayProvider] Failed to fetch purchase:", err);
|
|
624
|
-
if (inFlightRef.current === cacheKey) {
|
|
625
|
-
setPurchaseError(err instanceof Error ? err : new Error(String(err)));
|
|
626
|
-
}
|
|
627
|
-
} finally {
|
|
628
|
-
if (inFlightRef.current === cacheKey) {
|
|
629
|
-
setLoading(false);
|
|
630
|
-
setIsRefetching(false);
|
|
631
|
-
inFlightRef.current = null;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
},
|
|
635
|
-
[isAuthenticated, internalCustomerRef, userId, purchaseData.purchases.length]
|
|
636
|
-
);
|
|
637
|
-
const fetchPurchaseRef = useRef(fetchPurchase);
|
|
638
|
-
useEffect(() => {
|
|
639
|
-
fetchPurchaseRef.current = fetchPurchase;
|
|
640
|
-
}, [fetchPurchase]);
|
|
641
|
-
const refetchPurchase = useCallback(async () => {
|
|
642
|
-
inFlightRef.current = null;
|
|
643
|
-
await fetchPurchaseRef.current(true);
|
|
644
|
-
}, []);
|
|
645
|
-
const cancelRenewal = useCallback(
|
|
646
|
-
async (params) => {
|
|
647
|
-
const currentConfig = configRef.current;
|
|
648
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
649
|
-
const route = currentConfig?.api?.cancelRenewal || "/api/cancel-renewal";
|
|
650
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
651
|
-
const res = await fetchFn(route, {
|
|
652
|
-
method: "POST",
|
|
653
|
-
headers,
|
|
654
|
-
body: JSON.stringify(params)
|
|
655
|
-
});
|
|
656
|
-
if (!res.ok) {
|
|
657
|
-
const data = await res.json().catch(() => ({}));
|
|
658
|
-
const error = new Error(data.error || `Failed to cancel renewal: ${res.statusText}`);
|
|
659
|
-
currentConfig?.onError?.(error, "cancelRenewal");
|
|
660
|
-
throw error;
|
|
661
|
-
}
|
|
662
|
-
const result = await res.json();
|
|
663
|
-
await refetchPurchase();
|
|
664
|
-
return result;
|
|
665
|
-
},
|
|
666
|
-
[refetchPurchase]
|
|
667
|
-
);
|
|
668
|
-
const reactivateRenewal = useCallback(
|
|
669
|
-
async (params) => {
|
|
670
|
-
const currentConfig = configRef.current;
|
|
671
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
672
|
-
const route = currentConfig?.api?.reactivateRenewal || "/api/reactivate-renewal";
|
|
673
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
674
|
-
const res = await fetchFn(route, {
|
|
675
|
-
method: "POST",
|
|
676
|
-
headers,
|
|
677
|
-
body: JSON.stringify(params)
|
|
678
|
-
});
|
|
679
|
-
if (!res.ok) {
|
|
680
|
-
const data = await res.json().catch(() => ({}));
|
|
681
|
-
const error = new Error(data.error || `Failed to reactivate renewal: ${res.statusText}`);
|
|
682
|
-
currentConfig?.onError?.(error, "reactivateRenewal");
|
|
683
|
-
throw error;
|
|
684
|
-
}
|
|
685
|
-
const result = await res.json();
|
|
686
|
-
await refetchPurchase();
|
|
687
|
-
return result;
|
|
688
|
-
},
|
|
689
|
-
[refetchPurchase]
|
|
690
|
-
);
|
|
691
|
-
const activatePlan = useCallback(
|
|
692
|
-
async (params) => {
|
|
693
|
-
const currentConfig = configRef.current;
|
|
694
|
-
const { headers } = await buildRequestHeaders(currentConfig);
|
|
695
|
-
const route = currentConfig?.api?.activatePlan || "/api/activate-plan";
|
|
696
|
-
const fetchFn = currentConfig?.fetch || fetch;
|
|
697
|
-
const res = await fetchFn(route, {
|
|
698
|
-
method: "POST",
|
|
699
|
-
headers,
|
|
700
|
-
body: JSON.stringify(params)
|
|
701
|
-
});
|
|
702
|
-
if (!res.ok) {
|
|
703
|
-
const data = await res.json().catch(() => ({}));
|
|
704
|
-
const error = new Error(data.error || `Failed to activate plan: ${res.statusText}`);
|
|
705
|
-
currentConfig?.onError?.(error, "activatePlan");
|
|
706
|
-
throw error;
|
|
707
|
-
}
|
|
708
|
-
const result = await res.json();
|
|
709
|
-
if (result.status === "activated" || result.status === "already_active") {
|
|
710
|
-
await refetchPurchase();
|
|
711
|
-
}
|
|
712
|
-
return result;
|
|
713
|
-
},
|
|
714
|
-
[refetchPurchase]
|
|
715
|
-
);
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
inFlightRef.current = null;
|
|
718
|
-
if (isAuthenticated || internalCustomerRef) {
|
|
719
|
-
fetchPurchase();
|
|
720
|
-
} else {
|
|
721
|
-
setPurchaseData({ purchases: [] });
|
|
722
|
-
setPurchaseError(null);
|
|
723
|
-
setLoading(false);
|
|
724
|
-
setIsRefetching(false);
|
|
725
|
-
}
|
|
726
|
-
}, [isAuthenticated, internalCustomerRef, userId]);
|
|
727
|
-
const updateCustomerRef = useCallback(
|
|
728
|
-
(newCustomerRef) => {
|
|
729
|
-
setInternalCustomerRef(newCustomerRef);
|
|
730
|
-
setCachedCustomerRef(newCustomerRef, userId);
|
|
731
|
-
fetchPurchase(true);
|
|
732
|
-
},
|
|
733
|
-
[fetchPurchase, userId]
|
|
734
|
-
);
|
|
735
|
-
const purchase = useMemo2(() => {
|
|
736
|
-
const activePurchase = getPrimaryPurchase(purchaseData.purchases);
|
|
737
|
-
const activePaidPurchases = purchaseData.purchases.filter(
|
|
738
|
-
(p) => p.status === "active" && isPaidPurchase(p)
|
|
739
|
-
);
|
|
740
|
-
const activePaidPurchase = activePaidPurchases.sort(
|
|
741
|
-
(a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
|
742
|
-
)[0] || null;
|
|
743
|
-
return {
|
|
744
|
-
loading,
|
|
745
|
-
isRefetching,
|
|
746
|
-
error: purchaseError,
|
|
747
|
-
customerRef: purchaseData.customerRef || internalCustomerRef,
|
|
748
|
-
email: purchaseData.email,
|
|
749
|
-
name: purchaseData.name,
|
|
750
|
-
purchases: purchaseData.purchases,
|
|
751
|
-
hasProduct: (productName) => {
|
|
752
|
-
return purchaseData.purchases.some(
|
|
753
|
-
(p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
|
|
754
|
-
);
|
|
755
|
-
},
|
|
756
|
-
hasPlan: (productName) => {
|
|
757
|
-
return purchaseData.purchases.some(
|
|
758
|
-
(p) => p.productName.toLowerCase() === productName.toLowerCase() && p.status === "active"
|
|
759
|
-
);
|
|
760
|
-
},
|
|
761
|
-
activePurchase,
|
|
762
|
-
hasPaidPurchase: activePaidPurchases.length > 0,
|
|
763
|
-
activePaidPurchase
|
|
764
|
-
};
|
|
765
|
-
}, [loading, isRefetching, purchaseError, purchaseData, internalCustomerRef]);
|
|
766
|
-
const balance = useMemo2(
|
|
767
|
-
() => ({
|
|
768
|
-
loading: balanceLoading,
|
|
769
|
-
credits: creditsValue,
|
|
770
|
-
displayCurrency: displayCurrencyValue,
|
|
771
|
-
creditsPerMinorUnit: creditsPerMinorUnitValue,
|
|
772
|
-
displayExchangeRate: displayExchangeRateValue,
|
|
773
|
-
refetch: fetchBalanceImpl,
|
|
774
|
-
adjustBalance: adjustBalanceImpl
|
|
775
|
-
}),
|
|
776
|
-
[balanceLoading, creditsValue, displayCurrencyValue, creditsPerMinorUnitValue, displayExchangeRateValue, fetchBalanceImpl, adjustBalanceImpl]
|
|
777
|
-
);
|
|
778
|
-
const contextValue = useMemo2(
|
|
779
|
-
() => ({
|
|
780
|
-
purchase,
|
|
781
|
-
refetchPurchase,
|
|
782
|
-
createPayment,
|
|
783
|
-
processPayment,
|
|
784
|
-
createTopupPayment,
|
|
785
|
-
cancelRenewal,
|
|
786
|
-
reactivateRenewal,
|
|
787
|
-
activatePlan,
|
|
788
|
-
customerRef: purchaseData.customerRef || internalCustomerRef,
|
|
789
|
-
updateCustomerRef,
|
|
790
|
-
balance,
|
|
791
|
-
_config: configRef.current
|
|
792
|
-
}),
|
|
793
|
-
[
|
|
794
|
-
purchase,
|
|
795
|
-
refetchPurchase,
|
|
796
|
-
createPayment,
|
|
797
|
-
processPayment,
|
|
798
|
-
createTopupPayment,
|
|
799
|
-
cancelRenewal,
|
|
800
|
-
reactivateRenewal,
|
|
801
|
-
activatePlan,
|
|
802
|
-
purchaseData.customerRef,
|
|
803
|
-
internalCustomerRef,
|
|
804
|
-
updateCustomerRef,
|
|
805
|
-
balance
|
|
806
|
-
]
|
|
807
|
-
);
|
|
808
|
-
return /* @__PURE__ */ jsx2(SolvaPayContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx2(CopyProvider, { locale: config?.locale, copy: config?.copy, children }) });
|
|
809
|
-
};
|
|
810
|
-
|
|
811
|
-
// src/hooks/useSolvaPay.ts
|
|
812
|
-
import { useContext as useContext2 } from "react";
|
|
813
|
-
function useSolvaPay() {
|
|
814
|
-
const context = useContext2(SolvaPayContext);
|
|
815
|
-
if (!context) {
|
|
816
|
-
throw new Error(
|
|
817
|
-
"useSolvaPay must be used within a SolvaPayProvider. Wrap your component tree with <SolvaPayProvider> to use this hook."
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
return context;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// src/hooks/usePurchase.ts
|
|
824
|
-
function usePurchase() {
|
|
825
|
-
const { purchase, refetchPurchase } = useSolvaPay();
|
|
826
|
-
return {
|
|
827
|
-
...purchase,
|
|
828
|
-
refetch: refetchPurchase
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// src/hooks/useCopy.ts
|
|
833
|
-
function useCopy() {
|
|
834
|
-
return useCopyContext().copy;
|
|
835
|
-
}
|
|
836
|
-
function useLocale() {
|
|
837
|
-
return useCopyContext().locale;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/hooks/usePlans.ts
|
|
841
|
-
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useMemo as useMemo3, useRef as useRef2 } from "react";
|
|
842
|
-
var plansCache = /* @__PURE__ */ new Map();
|
|
843
|
-
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
844
|
-
function processPlans(raw, filter, sortBy) {
|
|
845
|
-
let result = sortBy ? [...raw].sort(sortBy) : raw;
|
|
846
|
-
if (filter) result = result.filter(filter);
|
|
847
|
-
return result;
|
|
848
|
-
}
|
|
849
|
-
function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
|
|
850
|
-
if (plans.length === 0) return 0;
|
|
851
|
-
if (initialPlanRef) {
|
|
852
|
-
const idx = plans.findIndex((p) => p.reference === initialPlanRef);
|
|
853
|
-
if (idx >= 0) return idx;
|
|
854
|
-
}
|
|
855
|
-
if (autoSelectFirstPaid) {
|
|
856
|
-
const idx = plans.findIndex((p) => p.requiresPayment !== false);
|
|
857
|
-
return idx >= 0 ? idx : 0;
|
|
858
|
-
}
|
|
859
|
-
return 0;
|
|
860
|
-
}
|
|
861
|
-
function usePlans(options) {
|
|
862
|
-
const {
|
|
863
|
-
fetcher,
|
|
864
|
-
productRef,
|
|
865
|
-
filter,
|
|
866
|
-
sortBy,
|
|
867
|
-
autoSelectFirstPaid = false,
|
|
868
|
-
initialPlanRef,
|
|
869
|
-
selectionReady = true
|
|
870
|
-
} = options;
|
|
871
|
-
const fetcherRef = useRef2(fetcher);
|
|
872
|
-
const filterRef = useRef2(filter);
|
|
873
|
-
const sortByRef = useRef2(sortBy);
|
|
874
|
-
const autoSelectFirstPaidRef = useRef2(autoSelectFirstPaid);
|
|
875
|
-
const initialPlanRefRef = useRef2(initialPlanRef);
|
|
876
|
-
const selectionReadyRef = useRef2(selectionReady);
|
|
877
|
-
const hasAppliedInitialRef = useRef2(false);
|
|
878
|
-
const userHasSelectedRef = useRef2(false);
|
|
879
|
-
const [selectedPlanIndex, setSelectedPlanIndexState] = useState2(() => {
|
|
880
|
-
if (!selectionReady || !productRef) return 0;
|
|
881
|
-
const cached = plansCache.get(productRef);
|
|
882
|
-
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
883
|
-
return 0;
|
|
884
|
-
}
|
|
885
|
-
const processed = processPlans(cached.plans, filter, sortBy);
|
|
886
|
-
if (processed.length === 0) return 0;
|
|
887
|
-
const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
|
|
888
|
-
hasAppliedInitialRef.current = true;
|
|
889
|
-
return idx;
|
|
890
|
-
});
|
|
891
|
-
const [plans, setPlans] = useState2(() => {
|
|
892
|
-
if (!productRef) return [];
|
|
893
|
-
const cached = plansCache.get(productRef);
|
|
894
|
-
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
895
|
-
return [];
|
|
896
|
-
}
|
|
897
|
-
return processPlans(cached.plans, filter, sortBy);
|
|
898
|
-
});
|
|
899
|
-
const [loading, setLoading] = useState2(() => plans.length === 0);
|
|
900
|
-
const [error, setError] = useState2(null);
|
|
901
|
-
useEffect2(() => {
|
|
902
|
-
fetcherRef.current = fetcher;
|
|
903
|
-
}, [fetcher]);
|
|
904
|
-
useEffect2(() => {
|
|
905
|
-
filterRef.current = filter;
|
|
906
|
-
}, [filter]);
|
|
907
|
-
useEffect2(() => {
|
|
908
|
-
sortByRef.current = sortBy;
|
|
909
|
-
}, [sortBy]);
|
|
910
|
-
useEffect2(() => {
|
|
911
|
-
autoSelectFirstPaidRef.current = autoSelectFirstPaid;
|
|
912
|
-
}, [autoSelectFirstPaid]);
|
|
913
|
-
useEffect2(() => {
|
|
914
|
-
initialPlanRefRef.current = initialPlanRef;
|
|
915
|
-
}, [initialPlanRef]);
|
|
916
|
-
useEffect2(() => {
|
|
917
|
-
selectionReadyRef.current = selectionReady;
|
|
918
|
-
}, [selectionReady]);
|
|
919
|
-
const setSelectedPlanIndex = useCallback2((index) => {
|
|
920
|
-
userHasSelectedRef.current = true;
|
|
921
|
-
setSelectedPlanIndexState(index);
|
|
922
|
-
}, []);
|
|
923
|
-
const applyInitialSelection = useCallback2((processedPlans) => {
|
|
924
|
-
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
925
|
-
if (!selectionReadyRef.current || processedPlans.length === 0) return;
|
|
926
|
-
hasAppliedInitialRef.current = true;
|
|
927
|
-
const idx = computeInitialIndex(
|
|
928
|
-
processedPlans,
|
|
929
|
-
initialPlanRefRef.current,
|
|
930
|
-
autoSelectFirstPaidRef.current
|
|
931
|
-
);
|
|
932
|
-
setSelectedPlanIndexState(idx);
|
|
933
|
-
}, []);
|
|
934
|
-
const fetchPlans = useCallback2(
|
|
935
|
-
async (force = false) => {
|
|
936
|
-
if (!productRef) {
|
|
937
|
-
setError(new Error("Product reference not configured"));
|
|
938
|
-
setLoading(false);
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
const cached = plansCache.get(productRef);
|
|
942
|
-
const now = Date.now();
|
|
943
|
-
if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
|
|
944
|
-
const processedPlans = processPlans(
|
|
945
|
-
cached.plans,
|
|
946
|
-
filterRef.current,
|
|
947
|
-
sortByRef.current
|
|
948
|
-
);
|
|
949
|
-
setPlans(processedPlans);
|
|
950
|
-
setLoading(false);
|
|
951
|
-
setError(null);
|
|
952
|
-
applyInitialSelection(processedPlans);
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
if (cached?.promise) {
|
|
956
|
-
try {
|
|
957
|
-
setLoading(true);
|
|
958
|
-
const fetchedPlans = await cached.promise;
|
|
959
|
-
const processedPlans = processPlans(
|
|
960
|
-
fetchedPlans,
|
|
961
|
-
filterRef.current,
|
|
962
|
-
sortByRef.current
|
|
963
|
-
);
|
|
964
|
-
setPlans(processedPlans);
|
|
965
|
-
setError(null);
|
|
966
|
-
applyInitialSelection(processedPlans);
|
|
967
|
-
} catch (err) {
|
|
968
|
-
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
969
|
-
} finally {
|
|
970
|
-
setLoading(false);
|
|
971
|
-
}
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
try {
|
|
975
|
-
setLoading(true);
|
|
976
|
-
setError(null);
|
|
977
|
-
const fetchPromise = fetcherRef.current(productRef);
|
|
978
|
-
plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
|
|
979
|
-
const fetchedPlans = await fetchPromise;
|
|
980
|
-
plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
|
|
981
|
-
const processedPlans = processPlans(
|
|
982
|
-
fetchedPlans,
|
|
983
|
-
filterRef.current,
|
|
984
|
-
sortByRef.current
|
|
985
|
-
);
|
|
986
|
-
setPlans(processedPlans);
|
|
987
|
-
applyInitialSelection(processedPlans);
|
|
988
|
-
} catch (err) {
|
|
989
|
-
plansCache.delete(productRef);
|
|
990
|
-
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
991
|
-
} finally {
|
|
992
|
-
setLoading(false);
|
|
993
|
-
}
|
|
994
|
-
},
|
|
995
|
-
[productRef, applyInitialSelection]
|
|
996
|
-
);
|
|
997
|
-
useEffect2(() => {
|
|
998
|
-
fetchPlans();
|
|
999
|
-
}, [fetchPlans]);
|
|
1000
|
-
useEffect2(() => {
|
|
1001
|
-
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
1002
|
-
if (!selectionReady || plans.length === 0) return;
|
|
1003
|
-
applyInitialSelection(plans);
|
|
1004
|
-
}, [selectionReady, plans, applyInitialSelection]);
|
|
1005
|
-
const selectedPlan = useMemo3(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
|
|
1006
|
-
const selectPlan = useCallback2(
|
|
1007
|
-
(planRef) => {
|
|
1008
|
-
const index = plans.findIndex((p) => p.reference === planRef);
|
|
1009
|
-
if (index >= 0) {
|
|
1010
|
-
setSelectedPlanIndex(index);
|
|
1011
|
-
}
|
|
1012
|
-
},
|
|
1013
|
-
[plans, setSelectedPlanIndex]
|
|
1014
|
-
);
|
|
1015
|
-
return {
|
|
1016
|
-
plans,
|
|
1017
|
-
loading,
|
|
1018
|
-
error,
|
|
1019
|
-
selectedPlanIndex,
|
|
1020
|
-
selectedPlan,
|
|
1021
|
-
setSelectedPlanIndex,
|
|
1022
|
-
selectPlan,
|
|
1023
|
-
refetch: () => fetchPlans(true),
|
|
1024
|
-
isSelectionReady: hasAppliedInitialRef.current
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// src/hooks/usePlan.ts
|
|
1029
|
-
import { useCallback as useCallback3, useEffect as useEffect3, useState as useState3 } from "react";
|
|
1030
|
-
async function listPlans(productRef, config) {
|
|
1031
|
-
const base = config?.api?.listPlans || "/api/list-plans";
|
|
1032
|
-
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
1033
|
-
const fetchFn = config?.fetch || fetch;
|
|
1034
|
-
const { headers } = await buildRequestHeaders(config);
|
|
1035
|
-
const res = await fetchFn(url, { method: "GET", headers });
|
|
1036
|
-
if (!res.ok) {
|
|
1037
|
-
const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
|
|
1038
|
-
config?.onError?.(error, "listPlans");
|
|
1039
|
-
throw error;
|
|
1040
|
-
}
|
|
1041
|
-
const data = await res.json();
|
|
1042
|
-
return data.plans ?? [];
|
|
1043
|
-
}
|
|
1044
|
-
function usePlan(options) {
|
|
1045
|
-
const { planRef, productRef } = options;
|
|
1046
|
-
const { _config } = useSolvaPay();
|
|
1047
|
-
const findPlan = useCallback3(
|
|
1048
|
-
(plans) => {
|
|
1049
|
-
if (!planRef) return null;
|
|
1050
|
-
return plans.find((p) => p.reference === planRef) || null;
|
|
1051
|
-
},
|
|
1052
|
-
[planRef]
|
|
1053
|
-
);
|
|
1054
|
-
const [plan, setPlan] = useState3(() => {
|
|
1055
|
-
if (!planRef || !productRef) return null;
|
|
1056
|
-
const cached = plansCache.get(productRef);
|
|
1057
|
-
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2) return null;
|
|
1058
|
-
return findPlan(cached.plans);
|
|
1059
|
-
});
|
|
1060
|
-
const [loading, setLoading] = useState3(() => !!(planRef && !plan));
|
|
1061
|
-
const [error, setError] = useState3(null);
|
|
1062
|
-
const load = useCallback3(
|
|
1063
|
-
async (force = false) => {
|
|
1064
|
-
if (!planRef) {
|
|
1065
|
-
setPlan(null);
|
|
1066
|
-
setLoading(false);
|
|
1067
|
-
setError(null);
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
if (!productRef) {
|
|
1071
|
-
setLoading(false);
|
|
1072
|
-
setError(
|
|
1073
|
-
new Error("usePlan: productRef is required to resolve a plan reference")
|
|
1074
|
-
);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
const cached = plansCache.get(productRef);
|
|
1078
|
-
const now = Date.now();
|
|
1079
|
-
if (!force && cached?.plans.length && now - cached.timestamp < CACHE_DURATION2) {
|
|
1080
|
-
const p = findPlan(cached.plans);
|
|
1081
|
-
setPlan(p);
|
|
1082
|
-
setLoading(false);
|
|
1083
|
-
setError(
|
|
1084
|
-
p ? null : new Error(
|
|
1085
|
-
`Plan "${planRef}" not found in product "${productRef}"`
|
|
1086
|
-
)
|
|
1087
|
-
);
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
if (!force && cached?.promise) {
|
|
1091
|
-
try {
|
|
1092
|
-
setLoading(true);
|
|
1093
|
-
const plans = await cached.promise;
|
|
1094
|
-
const p = findPlan(plans);
|
|
1095
|
-
setPlan(p);
|
|
1096
|
-
setError(
|
|
1097
|
-
p ? null : new Error(
|
|
1098
|
-
`Plan "${planRef}" not found in product "${productRef}"`
|
|
1099
|
-
)
|
|
1100
|
-
);
|
|
1101
|
-
} catch (err) {
|
|
1102
|
-
setError(err instanceof Error ? err : new Error("Failed to load plan"));
|
|
1103
|
-
} finally {
|
|
1104
|
-
setLoading(false);
|
|
1105
|
-
}
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
try {
|
|
1109
|
-
setLoading(true);
|
|
1110
|
-
setError(null);
|
|
1111
|
-
const promise = listPlans(productRef, _config);
|
|
1112
|
-
plansCache.set(productRef, { plans: [], timestamp: now, promise });
|
|
1113
|
-
const plans = await promise;
|
|
1114
|
-
plansCache.set(productRef, { plans, timestamp: now, promise: null });
|
|
1115
|
-
const p = findPlan(plans);
|
|
1116
|
-
setPlan(p);
|
|
1117
|
-
if (!p) {
|
|
1118
|
-
setError(new Error(`Plan "${planRef}" not found in product "${productRef}"`));
|
|
1119
|
-
}
|
|
1120
|
-
} catch (err) {
|
|
1121
|
-
plansCache.delete(productRef);
|
|
1122
|
-
setError(err instanceof Error ? err : new Error("Failed to load plan"));
|
|
1123
|
-
} finally {
|
|
1124
|
-
setLoading(false);
|
|
1125
|
-
}
|
|
1126
|
-
},
|
|
1127
|
-
[planRef, productRef, _config, findPlan]
|
|
1128
|
-
);
|
|
1129
|
-
useEffect3(() => {
|
|
1130
|
-
load();
|
|
1131
|
-
}, [load]);
|
|
1132
|
-
return {
|
|
1133
|
-
plan,
|
|
1134
|
-
loading,
|
|
1135
|
-
error,
|
|
1136
|
-
refetch: () => load(true)
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// src/primitives/composeRefs.ts
|
|
1141
|
-
function setRef(ref, value) {
|
|
1142
|
-
if (typeof ref === "function") {
|
|
1143
|
-
ref(value);
|
|
1144
|
-
} else if (ref !== null && ref !== void 0) {
|
|
1145
|
-
;
|
|
1146
|
-
ref.current = value;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
function composeRefs(...refs) {
|
|
1150
|
-
return (node) => {
|
|
1151
|
-
for (const ref of refs) {
|
|
1152
|
-
setRef(ref, node);
|
|
1153
|
-
}
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// src/primitives/slot.tsx
|
|
1158
|
-
import React3 from "react";
|
|
1159
|
-
import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
|
|
1160
|
-
var Slot = React3.forwardRef((props, forwardedRef) => {
|
|
1161
|
-
const { children, ...slotProps } = props;
|
|
1162
|
-
const childrenArray = React3.Children.toArray(children);
|
|
1163
|
-
const slottable = childrenArray.find(isSlottable);
|
|
1164
|
-
if (slottable) {
|
|
1165
|
-
const newElement = slottable.props.children;
|
|
1166
|
-
const newChildren = childrenArray.map((child) => {
|
|
1167
|
-
if (child === slottable) {
|
|
1168
|
-
if (React3.Children.count(newElement) > 1) return React3.Children.only(null);
|
|
1169
|
-
return React3.isValidElement(newElement) ? newElement.props.children : null;
|
|
1170
|
-
}
|
|
1171
|
-
return child;
|
|
1172
|
-
});
|
|
1173
|
-
return /* @__PURE__ */ jsx3(SlotClone, { ...slotProps, ref: forwardedRef, children: React3.isValidElement(newElement) ? React3.cloneElement(newElement, void 0, newChildren) : null });
|
|
1174
|
-
}
|
|
1175
|
-
return /* @__PURE__ */ jsx3(SlotClone, { ...slotProps, ref: forwardedRef, children });
|
|
1176
|
-
});
|
|
1177
|
-
Slot.displayName = "Slot";
|
|
1178
|
-
var SlotClone = React3.forwardRef(
|
|
1179
|
-
(props, forwardedRef) => {
|
|
1180
|
-
const { children, ...slotProps } = props;
|
|
1181
|
-
if (React3.isValidElement(children)) {
|
|
1182
|
-
const childProps = children.props ?? {};
|
|
1183
|
-
const childRef = getElementRef(children);
|
|
1184
|
-
const props2 = mergeProps(slotProps, childProps);
|
|
1185
|
-
const cloneProps = { ...props2 };
|
|
1186
|
-
if (forwardedRef) {
|
|
1187
|
-
cloneProps.ref = childRef ? composeRefs(forwardedRef, childRef) : forwardedRef;
|
|
1188
|
-
} else {
|
|
1189
|
-
cloneProps.ref = childRef;
|
|
1190
|
-
}
|
|
1191
|
-
return React3.cloneElement(children, cloneProps);
|
|
1192
|
-
}
|
|
1193
|
-
return React3.Children.count(children) > 1 ? React3.Children.only(null) : null;
|
|
1194
|
-
}
|
|
1195
|
-
);
|
|
1196
|
-
SlotClone.displayName = "SlotClone";
|
|
1197
|
-
var Slottable = ({ children }) => {
|
|
1198
|
-
return /* @__PURE__ */ jsx3(Fragment, { children });
|
|
1199
|
-
};
|
|
1200
|
-
function isSlottable(child) {
|
|
1201
|
-
return React3.isValidElement(child) && child.type === Slottable;
|
|
1202
|
-
}
|
|
1203
|
-
function mergeProps(slotProps, childProps) {
|
|
1204
|
-
const overrideProps = { ...childProps };
|
|
1205
|
-
for (const propName in slotProps) {
|
|
1206
|
-
const slotPropValue = slotProps[propName];
|
|
1207
|
-
const childPropValue = childProps[propName];
|
|
1208
|
-
const isHandler = /^on[A-Z]/.test(propName);
|
|
1209
|
-
if (isHandler) {
|
|
1210
|
-
if (slotPropValue && childPropValue) {
|
|
1211
|
-
overrideProps[propName] = (...args) => {
|
|
1212
|
-
;
|
|
1213
|
-
childPropValue(...args);
|
|
1214
|
-
const first = args[0];
|
|
1215
|
-
if (!first?.defaultPrevented) {
|
|
1216
|
-
;
|
|
1217
|
-
slotPropValue(...args);
|
|
1218
|
-
}
|
|
1219
|
-
};
|
|
1220
|
-
} else if (slotPropValue) {
|
|
1221
|
-
overrideProps[propName] = slotPropValue;
|
|
1222
|
-
}
|
|
1223
|
-
} else if (propName === "style") {
|
|
1224
|
-
overrideProps[propName] = {
|
|
1225
|
-
...slotPropValue,
|
|
1226
|
-
...childPropValue
|
|
1227
|
-
};
|
|
1228
|
-
} else if (propName === "className") {
|
|
1229
|
-
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
|
|
1230
|
-
} else if (propName === "ref") {
|
|
1231
|
-
} else if (childPropValue === void 0) {
|
|
1232
|
-
overrideProps[propName] = slotPropValue;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return overrideProps;
|
|
1236
|
-
}
|
|
1237
|
-
function getElementRef(element) {
|
|
1238
|
-
const el = element;
|
|
1239
|
-
return el.props?.ref ?? el.ref ?? null;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// src/hooks/useProduct.ts
|
|
1243
|
-
import { useCallback as useCallback4, useEffect as useEffect4, useState as useState4 } from "react";
|
|
1244
|
-
var productCache = /* @__PURE__ */ new Map();
|
|
1245
|
-
var CACHE_DURATION3 = 5 * 60 * 1e3;
|
|
1246
|
-
function routeFor(config) {
|
|
1247
|
-
return config?.api?.getProduct || "/api/get-product";
|
|
1248
|
-
}
|
|
1249
|
-
async function fetchProduct(productRef, config) {
|
|
1250
|
-
const base = routeFor(config);
|
|
1251
|
-
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
1252
|
-
const fetchFn = config?.fetch || fetch;
|
|
1253
|
-
const { headers } = await buildRequestHeaders(config);
|
|
1254
|
-
const res = await fetchFn(url, { method: "GET", headers });
|
|
1255
|
-
if (!res.ok) {
|
|
1256
|
-
const error = new Error(`Failed to fetch product: ${res.statusText || res.status}`);
|
|
1257
|
-
config?.onError?.(error, "getProduct");
|
|
1258
|
-
throw error;
|
|
1259
|
-
}
|
|
1260
|
-
return await res.json();
|
|
1261
|
-
}
|
|
1262
|
-
function useProduct(productRef) {
|
|
1263
|
-
const { _config } = useSolvaPay();
|
|
1264
|
-
const cacheKey = productRef || "";
|
|
1265
|
-
const [product, setProduct] = useState4(
|
|
1266
|
-
() => productRef ? productCache.get(cacheKey)?.product ?? null : null
|
|
1267
|
-
);
|
|
1268
|
-
const [loading, setLoading] = useState4(() => {
|
|
1269
|
-
if (!productRef) return false;
|
|
1270
|
-
const cached = productCache.get(cacheKey);
|
|
1271
|
-
return !cached || !cached.product && !cached.promise;
|
|
1272
|
-
});
|
|
1273
|
-
const [error, setError] = useState4(null);
|
|
1274
|
-
const load = useCallback4(
|
|
1275
|
-
async (force = false) => {
|
|
1276
|
-
if (!productRef) {
|
|
1277
|
-
setProduct(null);
|
|
1278
|
-
setLoading(false);
|
|
1279
|
-
setError(null);
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
const cached = productCache.get(productRef);
|
|
1283
|
-
const now = Date.now();
|
|
1284
|
-
if (!force && cached?.product && now - cached.timestamp < CACHE_DURATION3) {
|
|
1285
|
-
setProduct(cached.product);
|
|
1286
|
-
setLoading(false);
|
|
1287
|
-
setError(null);
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
if (!force && cached?.promise) {
|
|
1291
|
-
try {
|
|
1292
|
-
setLoading(true);
|
|
1293
|
-
const p = await cached.promise;
|
|
1294
|
-
setProduct(p);
|
|
1295
|
-
setError(null);
|
|
1296
|
-
} catch (err) {
|
|
1297
|
-
setError(err instanceof Error ? err : new Error("Failed to load product"));
|
|
1298
|
-
} finally {
|
|
1299
|
-
setLoading(false);
|
|
1300
|
-
}
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
try {
|
|
1304
|
-
setLoading(true);
|
|
1305
|
-
setError(null);
|
|
1306
|
-
const promise = fetchProduct(productRef, _config);
|
|
1307
|
-
productCache.set(productRef, { product: null, promise, timestamp: now });
|
|
1308
|
-
const p = await promise;
|
|
1309
|
-
productCache.set(productRef, { product: p, promise: null, timestamp: now });
|
|
1310
|
-
setProduct(p);
|
|
1311
|
-
} catch (err) {
|
|
1312
|
-
productCache.delete(productRef);
|
|
1313
|
-
setError(err instanceof Error ? err : new Error("Failed to load product"));
|
|
1314
|
-
} finally {
|
|
1315
|
-
setLoading(false);
|
|
1316
|
-
}
|
|
1317
|
-
},
|
|
1318
|
-
[productRef, _config]
|
|
1319
|
-
);
|
|
1320
|
-
useEffect4(() => {
|
|
1321
|
-
load();
|
|
1322
|
-
}, [load]);
|
|
1323
|
-
return {
|
|
1324
|
-
product,
|
|
1325
|
-
loading,
|
|
1326
|
-
error,
|
|
1327
|
-
refetch: () => load(true)
|
|
1328
|
-
};
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// src/utils/format.ts
|
|
1332
|
-
var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
1333
|
-
"bif",
|
|
1334
|
-
"clp",
|
|
1335
|
-
"djf",
|
|
1336
|
-
"gnf",
|
|
1337
|
-
"jpy",
|
|
1338
|
-
"kmf",
|
|
1339
|
-
"krw",
|
|
1340
|
-
"mga",
|
|
1341
|
-
"pyg",
|
|
1342
|
-
"rwf",
|
|
1343
|
-
"ugx",
|
|
1344
|
-
"vnd",
|
|
1345
|
-
"vuv",
|
|
1346
|
-
"xaf",
|
|
1347
|
-
"xof",
|
|
1348
|
-
"xpf"
|
|
1349
|
-
]);
|
|
1350
|
-
function getFractionDigits(currency) {
|
|
1351
|
-
return ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase()) ? 0 : 2;
|
|
1352
|
-
}
|
|
1353
|
-
function toMajorUnits(amountMinor, currency) {
|
|
1354
|
-
const fractionDigits = getFractionDigits(currency);
|
|
1355
|
-
return fractionDigits === 0 ? amountMinor : amountMinor / 100;
|
|
1356
|
-
}
|
|
1357
|
-
function formatPrice(amountMinor, currency, opts = {}) {
|
|
1358
|
-
const { locale, interval, intervalCount = 1, free = "Free" } = opts;
|
|
1359
|
-
if (amountMinor === 0 && free !== "") {
|
|
1360
|
-
return free;
|
|
1361
|
-
}
|
|
1362
|
-
const fractionDigits = getFractionDigits(currency);
|
|
1363
|
-
const major = toMajorUnits(amountMinor, currency);
|
|
1364
|
-
const formatter = new Intl.NumberFormat(locale, {
|
|
1365
|
-
style: "currency",
|
|
1366
|
-
currency: currency.toUpperCase(),
|
|
1367
|
-
minimumFractionDigits: fractionDigits,
|
|
1368
|
-
maximumFractionDigits: fractionDigits
|
|
1369
|
-
});
|
|
1370
|
-
const formatted = formatter.format(major);
|
|
1371
|
-
if (!interval) return formatted;
|
|
1372
|
-
const suffix = intervalCount > 1 ? `${intervalCount} ${interval}s` : interval;
|
|
1373
|
-
return `${formatted} / ${suffix}`;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/i18n/interpolate.ts
|
|
1377
|
-
function interpolate(template, vars) {
|
|
1378
|
-
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
1379
|
-
const value = vars[key];
|
|
1380
|
-
if (value === void 0 || value === null) return match;
|
|
1381
|
-
return String(value);
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// src/primitives/CheckoutSummary.tsx
|
|
1386
|
-
import { createContext as createContext4, forwardRef, useContext as useContext4, useMemo as useMemo4 } from "react";
|
|
1387
|
-
|
|
1388
|
-
// src/components/PlanSelectionContext.tsx
|
|
1389
|
-
import { createContext as createContext3, useContext as useContext3 } from "react";
|
|
1390
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1391
|
-
var PlanSelectionContext = createContext3(null);
|
|
1392
|
-
var PlanSelectionProvider = ({
|
|
1393
|
-
value,
|
|
1394
|
-
children
|
|
1395
|
-
}) => /* @__PURE__ */ jsx4(PlanSelectionContext.Provider, { value, children });
|
|
1396
|
-
function usePlanSelection() {
|
|
1397
|
-
return useContext3(PlanSelectionContext);
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// src/utils/errors.ts
|
|
1401
|
-
var DOCS_BASE_URL = "https://solvapay.com/docs";
|
|
1402
|
-
var SolvaPayError = class extends Error {
|
|
1403
|
-
code;
|
|
1404
|
-
docsUrl;
|
|
1405
|
-
constructor(code, message, docsUrl) {
|
|
1406
|
-
super(message);
|
|
1407
|
-
this.name = "SolvaPayError";
|
|
1408
|
-
this.code = code;
|
|
1409
|
-
this.docsUrl = docsUrl;
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1412
|
-
var MissingProviderError = class extends SolvaPayError {
|
|
1413
|
-
constructor(primitiveName) {
|
|
1414
|
-
const docsUrl = `${DOCS_BASE_URL}/troubleshooting/missing-provider`;
|
|
1415
|
-
super(
|
|
1416
|
-
"MISSING_PROVIDER",
|
|
1417
|
-
`${primitiveName} must be rendered inside <SolvaPayProvider>. Wrap your app (or the section using ${primitiveName}) with <SolvaPayProvider config={{ ... }}>. See ${docsUrl}`,
|
|
1418
|
-
docsUrl
|
|
1419
|
-
);
|
|
1420
|
-
this.name = "MissingProviderError";
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
var MissingProductRefError = class extends SolvaPayError {
|
|
1424
|
-
constructor(primitiveName) {
|
|
1425
|
-
const docsUrl = `${DOCS_BASE_URL}/primitives/product-ref`;
|
|
1426
|
-
super(
|
|
1427
|
-
"MISSING_PRODUCT_REF",
|
|
1428
|
-
`${primitiveName} requires a productRef. Pass the \`productRef\` prop (or set NEXT_PUBLIC_SOLVAPAY_PRODUCT_REF). See ${docsUrl}`,
|
|
1429
|
-
docsUrl
|
|
1430
|
-
);
|
|
1431
|
-
this.name = "MissingProductRefError";
|
|
1432
|
-
}
|
|
1433
|
-
};
|
|
1434
|
-
|
|
1435
|
-
// src/primitives/CheckoutSummary.tsx
|
|
1436
|
-
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1437
|
-
var CheckoutSummaryContext = createContext4(null);
|
|
1438
|
-
function useCheckoutSummaryContext(part) {
|
|
1439
|
-
const ctx = useContext4(CheckoutSummaryContext);
|
|
1440
|
-
if (!ctx) {
|
|
1441
|
-
throw new Error(
|
|
1442
|
-
`CheckoutSummary.${part} must be rendered inside <CheckoutSummary.Root>.`
|
|
1443
|
-
);
|
|
1444
|
-
}
|
|
1445
|
-
return ctx;
|
|
1446
|
-
}
|
|
1447
|
-
var Root = forwardRef(function CheckoutSummaryRoot({ planRef, productRef, asChild, children, ...rest }, forwardedRef) {
|
|
1448
|
-
const solva = useContext4(SolvaPayContext);
|
|
1449
|
-
if (!solva) throw new MissingProviderError("CheckoutSummary");
|
|
1450
|
-
const locale = useLocale();
|
|
1451
|
-
const copy = useCopy();
|
|
1452
|
-
const planSelection = usePlanSelection();
|
|
1453
|
-
const resolvedPlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
|
|
1454
|
-
const resolvedProductRef = productRef ?? planSelection?.productRef;
|
|
1455
|
-
const { plan, loading: planLoading } = usePlan({
|
|
1456
|
-
planRef: resolvedPlanRef,
|
|
1457
|
-
productRef: resolvedProductRef
|
|
1458
|
-
});
|
|
1459
|
-
const { product, loading: productLoading } = useProduct(resolvedProductRef);
|
|
1460
|
-
const loading = planLoading || productLoading;
|
|
1461
|
-
const priceFormatted = useMemo4(() => {
|
|
1462
|
-
const price = plan?.price ?? 0;
|
|
1463
|
-
const currency = plan?.currency ?? "usd";
|
|
1464
|
-
return formatPrice(price, currency, {
|
|
1465
|
-
locale,
|
|
1466
|
-
interval: plan?.interval,
|
|
1467
|
-
intervalCount: 1,
|
|
1468
|
-
free: copy.interval.free
|
|
1469
|
-
});
|
|
1470
|
-
}, [plan, locale, copy.interval.free]);
|
|
1471
|
-
const trialBanner = useMemo4(() => {
|
|
1472
|
-
const trialDays = plan?.trialDays;
|
|
1473
|
-
if (!trialDays || trialDays <= 0) return null;
|
|
1474
|
-
return interpolate(copy.interval.trial, { trialDays });
|
|
1475
|
-
}, [plan?.trialDays, copy.interval.trial]);
|
|
1476
|
-
const ctx = useMemo4(
|
|
1477
|
-
() => ({ plan, product, priceFormatted, trialBanner, loading }),
|
|
1478
|
-
[plan, product, priceFormatted, trialBanner, loading]
|
|
1479
|
-
);
|
|
1480
|
-
const Comp = asChild ? Slot : "div";
|
|
1481
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary": "", ...rest, children: /* @__PURE__ */ jsx5(CheckoutSummaryContext.Provider, { value: ctx, children }) });
|
|
1482
|
-
});
|
|
1483
|
-
var ProductSlot = forwardRef(function CheckoutSummaryProduct({ asChild, children, ...rest }, forwardedRef) {
|
|
1484
|
-
const ctx = useCheckoutSummaryContext("Product");
|
|
1485
|
-
const name = ctx.product?.name;
|
|
1486
|
-
if (!name) return null;
|
|
1487
|
-
const Comp = asChild ? Slot : "span";
|
|
1488
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-product": "", ...rest, children: children ?? name });
|
|
1489
|
-
});
|
|
1490
|
-
var PlanSlot = forwardRef(function CheckoutSummaryPlan({ asChild, children, ...rest }, forwardedRef) {
|
|
1491
|
-
const ctx = useCheckoutSummaryContext("Plan");
|
|
1492
|
-
const name = ctx.plan?.name;
|
|
1493
|
-
if (!name) return null;
|
|
1494
|
-
const Comp = asChild ? Slot : "span";
|
|
1495
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-plan": "", ...rest, children: children ?? name });
|
|
1496
|
-
});
|
|
1497
|
-
var PriceSlot = forwardRef(function CheckoutSummaryPrice({ asChild, children, ...rest }, forwardedRef) {
|
|
1498
|
-
const ctx = useCheckoutSummaryContext("Price");
|
|
1499
|
-
const Comp = asChild ? Slot : "span";
|
|
1500
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-price": "", ...rest, children: children ?? ctx.priceFormatted });
|
|
1501
|
-
});
|
|
1502
|
-
var TrialSlot = forwardRef(function CheckoutSummaryTrial({ asChild, children, ...rest }, forwardedRef) {
|
|
1503
|
-
const ctx = useCheckoutSummaryContext("Trial");
|
|
1504
|
-
if (!ctx.trialBanner) return null;
|
|
1505
|
-
const Comp = asChild ? Slot : "span";
|
|
1506
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-trial": "", ...rest, children: children ?? ctx.trialBanner });
|
|
1507
|
-
});
|
|
1508
|
-
var TaxNoteSlot = forwardRef(function CheckoutSummaryTaxNote({ asChild, children, ...rest }, forwardedRef) {
|
|
1509
|
-
useCheckoutSummaryContext("TaxNote");
|
|
1510
|
-
const Comp = asChild ? Slot : "span";
|
|
1511
|
-
return /* @__PURE__ */ jsx5(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-tax-note": "", ...rest, children: children ?? "Taxes calculated at checkout" });
|
|
1512
|
-
});
|
|
1513
|
-
var CheckoutSummaryRoot2 = Root;
|
|
1514
|
-
var CheckoutSummaryProduct2 = ProductSlot;
|
|
1515
|
-
var CheckoutSummaryPlan2 = PlanSlot;
|
|
1516
|
-
var CheckoutSummaryPrice2 = PriceSlot;
|
|
1517
|
-
var CheckoutSummaryTrial2 = TrialSlot;
|
|
1518
|
-
var CheckoutSummaryTaxNote2 = TaxNoteSlot;
|
|
1519
|
-
var CheckoutSummary = {
|
|
1520
|
-
Root,
|
|
1521
|
-
Product: ProductSlot,
|
|
1522
|
-
Plan: PlanSlot,
|
|
1523
|
-
Price: PriceSlot,
|
|
1524
|
-
Trial: TrialSlot,
|
|
1525
|
-
TaxNote: TaxNoteSlot
|
|
1526
|
-
};
|
|
1527
|
-
function useCheckoutSummary() {
|
|
1528
|
-
return useCheckoutSummaryContext("useCheckoutSummary");
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// src/primitives/composeEventHandlers.ts
|
|
1532
|
-
function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
|
|
1533
|
-
return function handleEvent(event) {
|
|
1534
|
-
originalEventHandler?.(event);
|
|
1535
|
-
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
|
|
1536
|
-
ourEventHandler?.(event);
|
|
1537
|
-
}
|
|
1538
|
-
};
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
// src/primitives/PlanSelector.tsx
|
|
1542
|
-
import {
|
|
1543
|
-
createContext as createContext5,
|
|
1544
|
-
forwardRef as forwardRef2,
|
|
1545
|
-
useCallback as useCallback5,
|
|
1546
|
-
useContext as useContext5,
|
|
1547
|
-
useMemo as useMemo5
|
|
1548
|
-
} from "react";
|
|
1549
|
-
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1550
|
-
var PlanSelectorContext = createContext5(null);
|
|
1551
|
-
function usePlanSelectorContext(part) {
|
|
1552
|
-
const ctx = useContext5(PlanSelectorContext);
|
|
1553
|
-
if (!ctx) {
|
|
1554
|
-
throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Root>.`);
|
|
1555
|
-
}
|
|
1556
|
-
return ctx;
|
|
1557
|
-
}
|
|
1558
|
-
var CardContext = createContext5(null);
|
|
1559
|
-
function useCardContext(part) {
|
|
1560
|
-
const ctx = useContext5(CardContext);
|
|
1561
|
-
if (!ctx) {
|
|
1562
|
-
throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Card>.`);
|
|
1563
|
-
}
|
|
1564
|
-
return ctx;
|
|
1565
|
-
}
|
|
1566
|
-
async function defaultListPlans(productRef, config) {
|
|
1567
|
-
const base = config?.api?.listPlans || "/api/list-plans";
|
|
1568
|
-
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
1569
|
-
const fetchFn = config?.fetch || fetch;
|
|
1570
|
-
const { headers } = await buildRequestHeaders(config);
|
|
1571
|
-
const res = await fetchFn(url, { method: "GET", headers });
|
|
1572
|
-
if (!res.ok) {
|
|
1573
|
-
const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
|
|
1574
|
-
config?.onError?.(error, "listPlans");
|
|
1575
|
-
throw error;
|
|
1576
|
-
}
|
|
1577
|
-
const data = await res.json();
|
|
1578
|
-
return data.plans ?? [];
|
|
1579
|
-
}
|
|
1580
|
-
var Root2 = forwardRef2(function PlanSelectorRoot(props, forwardedRef) {
|
|
1581
|
-
const {
|
|
1582
|
-
productRef,
|
|
1583
|
-
fetcher,
|
|
1584
|
-
filter,
|
|
1585
|
-
sortBy,
|
|
1586
|
-
autoSelectFirstPaid = true,
|
|
1587
|
-
initialPlanRef,
|
|
1588
|
-
currentPlanRef,
|
|
1589
|
-
popularPlanRef,
|
|
1590
|
-
onSelect,
|
|
1591
|
-
children,
|
|
1592
|
-
...rest
|
|
1593
|
-
} = props;
|
|
1594
|
-
const solva = useContext5(SolvaPayContext);
|
|
1595
|
-
if (!solva) throw new MissingProviderError("PlanSelector");
|
|
1596
|
-
if (!productRef) throw new MissingProductRefError("PlanSelector");
|
|
1597
|
-
const { _config } = solva;
|
|
1598
|
-
const { purchases } = usePurchase();
|
|
1599
|
-
const effectiveFetcher = useMemo5(
|
|
1600
|
-
() => fetcher ?? ((ref) => defaultListPlans(ref, _config)),
|
|
1601
|
-
[fetcher, _config]
|
|
1602
|
-
);
|
|
1603
|
-
const { plans, selectedPlan, selectPlan, loading, error } = usePlans({
|
|
1604
|
-
productRef,
|
|
1605
|
-
fetcher: effectiveFetcher,
|
|
1606
|
-
filter,
|
|
1607
|
-
sortBy,
|
|
1608
|
-
autoSelectFirstPaid,
|
|
1609
|
-
initialPlanRef
|
|
1610
|
-
});
|
|
1611
|
-
const autoCurrentPlanRef = useMemo5(() => {
|
|
1612
|
-
const active = purchases.filter((p) => p.status === "active").sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0];
|
|
1613
|
-
return active?.planSnapshot?.reference ?? null;
|
|
1614
|
-
}, [purchases]);
|
|
1615
|
-
const resolvedCurrentPlanRef = currentPlanRef === null ? null : currentPlanRef ?? autoCurrentPlanRef;
|
|
1616
|
-
const isCurrent = useCallback5(
|
|
1617
|
-
(ref) => resolvedCurrentPlanRef === ref,
|
|
1618
|
-
[resolvedCurrentPlanRef]
|
|
1619
|
-
);
|
|
1620
|
-
const isFree = useCallback5(
|
|
1621
|
-
(ref) => plans.find((p) => p.reference === ref)?.requiresPayment === false,
|
|
1622
|
-
[plans]
|
|
1623
|
-
);
|
|
1624
|
-
const isPopular = useCallback5(
|
|
1625
|
-
(ref) => popularPlanRef === ref,
|
|
1626
|
-
[popularPlanRef]
|
|
1627
|
-
);
|
|
1628
|
-
const select = useCallback5(
|
|
1629
|
-
(ref) => {
|
|
1630
|
-
const plan = plans.find((p) => p.reference === ref);
|
|
1631
|
-
if (!plan) return;
|
|
1632
|
-
if (plan.requiresPayment === false) return;
|
|
1633
|
-
selectPlan(ref);
|
|
1634
|
-
onSelect?.(ref, plan);
|
|
1635
|
-
},
|
|
1636
|
-
[plans, selectPlan, onSelect]
|
|
1637
|
-
);
|
|
1638
|
-
const selectedPlanRef = selectedPlan?.reference ?? null;
|
|
1639
|
-
const ctx = useMemo5(
|
|
1640
|
-
() => ({
|
|
1641
|
-
plans,
|
|
1642
|
-
loading,
|
|
1643
|
-
error,
|
|
1644
|
-
selectedPlan: selectedPlan ?? null,
|
|
1645
|
-
selectedPlanRef,
|
|
1646
|
-
popularPlanRef,
|
|
1647
|
-
currentPlanRef: resolvedCurrentPlanRef,
|
|
1648
|
-
isCurrent,
|
|
1649
|
-
isFree,
|
|
1650
|
-
isPopular,
|
|
1651
|
-
select
|
|
1652
|
-
}),
|
|
1653
|
-
[
|
|
1654
|
-
plans,
|
|
1655
|
-
loading,
|
|
1656
|
-
error,
|
|
1657
|
-
selectedPlan,
|
|
1658
|
-
selectedPlanRef,
|
|
1659
|
-
popularPlanRef,
|
|
1660
|
-
resolvedCurrentPlanRef,
|
|
1661
|
-
isCurrent,
|
|
1662
|
-
isFree,
|
|
1663
|
-
isPopular,
|
|
1664
|
-
select
|
|
1665
|
-
]
|
|
1666
|
-
);
|
|
1667
|
-
return /* @__PURE__ */ jsx6(
|
|
1668
|
-
PlanSelectionProvider,
|
|
1669
|
-
{
|
|
1670
|
-
value: {
|
|
1671
|
-
productRef,
|
|
1672
|
-
selectedPlanRef,
|
|
1673
|
-
setSelectedPlanRef: (ref) => {
|
|
1674
|
-
if (ref) select(ref);
|
|
1675
|
-
},
|
|
1676
|
-
plans,
|
|
1677
|
-
loading,
|
|
1678
|
-
error
|
|
1679
|
-
},
|
|
1680
|
-
children: /* @__PURE__ */ jsx6("div", { ref: forwardedRef, "data-solvapay-plan-selector": "", ...rest, children: /* @__PURE__ */ jsx6(PlanSelectorContext.Provider, { value: ctx, children }) })
|
|
1681
|
-
}
|
|
1682
|
-
);
|
|
1683
|
-
});
|
|
1684
|
-
var Heading = forwardRef2(function PlanSelectorHeading({ asChild, children, ...rest }, forwardedRef) {
|
|
1685
|
-
usePlanSelectorContext("Heading");
|
|
1686
|
-
const copy = useCopy();
|
|
1687
|
-
const Comp = asChild ? Slot : "h3";
|
|
1688
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-heading": "", ...rest, children: children ?? copy.planSelector.heading });
|
|
1689
|
-
});
|
|
1690
|
-
var Grid = forwardRef2(function PlanSelectorGrid({ children, ...rest }, forwardedRef) {
|
|
1691
|
-
const ctx = usePlanSelectorContext("Grid");
|
|
1692
|
-
return /* @__PURE__ */ jsx6("div", { ref: forwardedRef, "data-solvapay-plan-selector-grid": "", ...rest, children: ctx.plans.map((plan) => {
|
|
1693
|
-
const isCurrent = ctx.isCurrent(plan.reference);
|
|
1694
|
-
const isFree = ctx.isFree(plan.reference);
|
|
1695
|
-
const isPopular = ctx.isPopular(plan.reference);
|
|
1696
|
-
const selected = ctx.selectedPlanRef === plan.reference;
|
|
1697
|
-
const disabled = isCurrent || isFree;
|
|
1698
|
-
const state = isCurrent ? "current" : isFree ? "disabled" : selected ? "selected" : "idle";
|
|
1699
|
-
const cardCtx = {
|
|
1700
|
-
plan,
|
|
1701
|
-
state,
|
|
1702
|
-
isCurrent,
|
|
1703
|
-
isFree,
|
|
1704
|
-
isPopular,
|
|
1705
|
-
disabled,
|
|
1706
|
-
select: () => ctx.select(plan.reference)
|
|
1707
|
-
};
|
|
1708
|
-
return /* @__PURE__ */ jsx6(CardContext.Provider, { value: cardCtx, children }, plan.reference);
|
|
1709
|
-
}) });
|
|
1710
|
-
});
|
|
1711
|
-
var Card = forwardRef2(function PlanSelectorCard({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
1712
|
-
const card = useCardContext("Card");
|
|
1713
|
-
const dataTrial = !!(card.plan.trialDays && card.plan.trialDays > 0);
|
|
1714
|
-
const commonProps = {
|
|
1715
|
-
"data-solvapay-plan-selector-card": "",
|
|
1716
|
-
"data-state": card.state,
|
|
1717
|
-
"data-free": card.isFree ? "" : void 0,
|
|
1718
|
-
"data-popular": card.isPopular ? "" : void 0,
|
|
1719
|
-
"data-trial": dataTrial ? "" : void 0,
|
|
1720
|
-
"aria-disabled": card.disabled || void 0,
|
|
1721
|
-
onClick: composeEventHandlers(onClick, () => {
|
|
1722
|
-
if (!card.disabled) card.select();
|
|
1723
|
-
}),
|
|
1724
|
-
...rest
|
|
1725
|
-
};
|
|
1726
|
-
if (asChild) {
|
|
1727
|
-
return (
|
|
1728
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1729
|
-
/* @__PURE__ */ jsx6(Slot, { ref: forwardedRef, ...commonProps, children })
|
|
1730
|
-
);
|
|
1731
|
-
}
|
|
1732
|
-
return /* @__PURE__ */ jsx6(
|
|
1733
|
-
"button",
|
|
1734
|
-
{
|
|
1735
|
-
ref: forwardedRef,
|
|
1736
|
-
type: "button",
|
|
1737
|
-
disabled: card.disabled,
|
|
1738
|
-
...commonProps,
|
|
1739
|
-
children
|
|
1740
|
-
}
|
|
1741
|
-
);
|
|
1742
|
-
});
|
|
1743
|
-
var CardName = forwardRef2(function PlanSelectorCardName({ asChild, children, ...rest }, forwardedRef) {
|
|
1744
|
-
const card = useCardContext("CardName");
|
|
1745
|
-
if (!card.plan.name) return null;
|
|
1746
|
-
const Comp = asChild ? Slot : "span";
|
|
1747
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? card.plan.name });
|
|
1748
|
-
});
|
|
1749
|
-
var CardPrice = forwardRef2(function PlanSelectorCardPrice({ asChild, children, ...rest }, forwardedRef) {
|
|
1750
|
-
const card = useCardContext("CardPrice");
|
|
1751
|
-
const locale = useLocale();
|
|
1752
|
-
const copy = useCopy();
|
|
1753
|
-
const formatted = useMemo5(() => {
|
|
1754
|
-
if (card.isFree) return copy.planSelector.freeBadge;
|
|
1755
|
-
return formatPrice(card.plan.price ?? 0, card.plan.currency ?? "usd", {
|
|
1756
|
-
locale,
|
|
1757
|
-
free: copy.interval.free
|
|
1758
|
-
});
|
|
1759
|
-
}, [
|
|
1760
|
-
card.isFree,
|
|
1761
|
-
card.plan.price,
|
|
1762
|
-
card.plan.currency,
|
|
1763
|
-
locale,
|
|
1764
|
-
copy.planSelector.freeBadge,
|
|
1765
|
-
copy.interval.free
|
|
1766
|
-
]);
|
|
1767
|
-
const Comp = asChild ? Slot : "span";
|
|
1768
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-price": "", ...rest, children: children ?? formatted });
|
|
1769
|
-
});
|
|
1770
|
-
var CardInterval = forwardRef2(function PlanSelectorCardInterval({ asChild, children, ...rest }, forwardedRef) {
|
|
1771
|
-
const card = useCardContext("CardInterval");
|
|
1772
|
-
const copy = useCopy();
|
|
1773
|
-
if (card.isFree || !card.plan.interval) return null;
|
|
1774
|
-
const text = interpolate(copy.planSelector.perIntervalShort, { interval: card.plan.interval });
|
|
1775
|
-
const Comp = asChild ? Slot : "span";
|
|
1776
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-interval": "", ...rest, children: children ?? text });
|
|
1777
|
-
});
|
|
1778
|
-
var CardBadge = forwardRef2(function PlanSelectorCardBadge({ asChild, children, ...rest }, forwardedRef) {
|
|
1779
|
-
const card = useCardContext("CardBadge");
|
|
1780
|
-
const copy = useCopy();
|
|
1781
|
-
let variant = null;
|
|
1782
|
-
let label = "";
|
|
1783
|
-
if (card.isCurrent) {
|
|
1784
|
-
variant = "current";
|
|
1785
|
-
label = copy.planSelector.currentBadge;
|
|
1786
|
-
} else if (card.isPopular && !card.isFree) {
|
|
1787
|
-
variant = "popular";
|
|
1788
|
-
label = copy.planSelector.popularBadge;
|
|
1789
|
-
}
|
|
1790
|
-
if (!variant) return null;
|
|
1791
|
-
const Comp = asChild ? Slot : "span";
|
|
1792
|
-
return /* @__PURE__ */ jsx6(
|
|
1793
|
-
Comp,
|
|
1794
|
-
{
|
|
1795
|
-
ref: forwardedRef,
|
|
1796
|
-
"data-solvapay-plan-selector-card-badge": "",
|
|
1797
|
-
"data-variant": variant,
|
|
1798
|
-
...rest,
|
|
1799
|
-
children: children ?? label
|
|
1800
|
-
}
|
|
1801
|
-
);
|
|
1802
|
-
});
|
|
1803
|
-
var Loading = forwardRef2(function PlanSelectorLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
1804
|
-
const ctx = usePlanSelectorContext("Loading");
|
|
1805
|
-
if (!ctx.loading || ctx.plans.length > 0) return null;
|
|
1806
|
-
const Comp = asChild ? Slot : "div";
|
|
1807
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-loading": "", ...rest, children });
|
|
1808
|
-
});
|
|
1809
|
-
var ErrorSlot = forwardRef2(function PlanSelectorError({ asChild, children, ...rest }, forwardedRef) {
|
|
1810
|
-
const ctx = usePlanSelectorContext("Error");
|
|
1811
|
-
if (!ctx.error) return null;
|
|
1812
|
-
const Comp = asChild ? Slot : "div";
|
|
1813
|
-
return /* @__PURE__ */ jsx6(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-error": "", role: "alert", ...rest, children: children ?? ctx.error.message });
|
|
1814
|
-
});
|
|
1815
|
-
var PlanSelectorRoot2 = Root2;
|
|
1816
|
-
var PlanSelectorHeading2 = Heading;
|
|
1817
|
-
var PlanSelectorGrid2 = Grid;
|
|
1818
|
-
var PlanSelectorCard2 = Card;
|
|
1819
|
-
var PlanSelectorCardName2 = CardName;
|
|
1820
|
-
var PlanSelectorCardPrice2 = CardPrice;
|
|
1821
|
-
var PlanSelectorCardInterval2 = CardInterval;
|
|
1822
|
-
var PlanSelectorCardBadge2 = CardBadge;
|
|
1823
|
-
var PlanSelectorLoading2 = Loading;
|
|
1824
|
-
var PlanSelectorError2 = ErrorSlot;
|
|
1825
|
-
var PlanSelector = {
|
|
1826
|
-
Root: Root2,
|
|
1827
|
-
Heading,
|
|
1828
|
-
Grid,
|
|
1829
|
-
Card,
|
|
1830
|
-
CardName,
|
|
1831
|
-
CardPrice,
|
|
1832
|
-
CardInterval,
|
|
1833
|
-
CardBadge,
|
|
1834
|
-
Loading,
|
|
1835
|
-
Error: ErrorSlot
|
|
1836
|
-
};
|
|
1837
|
-
function usePlanSelector() {
|
|
1838
|
-
return usePlanSelectorContext("usePlanSelector");
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
export {
|
|
1842
|
-
filterPurchases,
|
|
1843
|
-
getActivePurchases,
|
|
1844
|
-
getCancelledPurchasesWithEndDate,
|
|
1845
|
-
getMostRecentPurchase,
|
|
1846
|
-
getPrimaryPurchase,
|
|
1847
|
-
isPaidPurchase,
|
|
1848
|
-
buildRequestHeaders,
|
|
1849
|
-
enCopy,
|
|
1850
|
-
mergeCopy,
|
|
1851
|
-
CopyContext,
|
|
1852
|
-
CopyProvider,
|
|
1853
|
-
SolvaPayProvider,
|
|
1854
|
-
useSolvaPay,
|
|
1855
|
-
usePurchase,
|
|
1856
|
-
useCopy,
|
|
1857
|
-
useLocale,
|
|
1858
|
-
usePlans,
|
|
1859
|
-
usePlan,
|
|
1860
|
-
setRef,
|
|
1861
|
-
composeRefs,
|
|
1862
|
-
Slot,
|
|
1863
|
-
Slottable,
|
|
1864
|
-
useProduct,
|
|
1865
|
-
formatPrice,
|
|
1866
|
-
interpolate,
|
|
1867
|
-
usePlanSelection,
|
|
1868
|
-
CheckoutSummaryRoot2 as CheckoutSummaryRoot,
|
|
1869
|
-
CheckoutSummaryProduct2 as CheckoutSummaryProduct,
|
|
1870
|
-
CheckoutSummaryPlan2 as CheckoutSummaryPlan,
|
|
1871
|
-
CheckoutSummaryPrice2 as CheckoutSummaryPrice,
|
|
1872
|
-
CheckoutSummaryTrial2 as CheckoutSummaryTrial,
|
|
1873
|
-
CheckoutSummaryTaxNote2 as CheckoutSummaryTaxNote,
|
|
1874
|
-
CheckoutSummary,
|
|
1875
|
-
useCheckoutSummary,
|
|
1876
|
-
composeEventHandlers,
|
|
1877
|
-
PlanSelectorRoot2 as PlanSelectorRoot,
|
|
1878
|
-
PlanSelectorHeading2 as PlanSelectorHeading,
|
|
1879
|
-
PlanSelectorGrid2 as PlanSelectorGrid,
|
|
1880
|
-
PlanSelectorCard2 as PlanSelectorCard,
|
|
1881
|
-
PlanSelectorCardName2 as PlanSelectorCardName,
|
|
1882
|
-
PlanSelectorCardPrice2 as PlanSelectorCardPrice,
|
|
1883
|
-
PlanSelectorCardInterval2 as PlanSelectorCardInterval,
|
|
1884
|
-
PlanSelectorCardBadge2 as PlanSelectorCardBadge,
|
|
1885
|
-
PlanSelectorLoading2 as PlanSelectorLoading,
|
|
1886
|
-
PlanSelectorError2 as PlanSelectorError,
|
|
1887
|
-
PlanSelector,
|
|
1888
|
-
usePlanSelector
|
|
1889
|
-
};
|