@solvapay/react 1.0.7 → 1.0.8-preview.10
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/CONTRIBUTING.md +117 -0
- package/README.md +234 -1
- package/dist/CancelPlanButton-CieT9swn.d.cts +1102 -0
- package/dist/CancelPlanButton-f56UlQN-.d.ts +1102 -0
- package/dist/chunk-MOP3ZBGC.js +4749 -0
- package/dist/index.cjs +4393 -1036
- package/dist/index.d.cts +375 -652
- package/dist/index.d.ts +375 -652
- package/dist/index.js +570 -1665
- package/dist/primitives/index.cjs +4254 -0
- package/dist/primitives/index.d.cts +533 -0
- package/dist/primitives/index.d.ts +533 -0
- package/dist/primitives/index.js +187 -0
- package/dist/styles.css +869 -0
- package/package.json +20 -5
|
@@ -0,0 +1,4254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/primitives/index.ts
|
|
31
|
+
var primitives_exports = {};
|
|
32
|
+
__export(primitives_exports, {
|
|
33
|
+
ActivationFlow: () => ActivationFlow,
|
|
34
|
+
ActivationFlowActivateButton: () => ActivationFlowActivateButton2,
|
|
35
|
+
ActivationFlowActivated: () => ActivationFlowActivated2,
|
|
36
|
+
ActivationFlowAmountPicker: () => ActivationFlowAmountPicker,
|
|
37
|
+
ActivationFlowContinueButton: () => ActivationFlowContinueButton2,
|
|
38
|
+
ActivationFlowError: () => ActivationFlowError2,
|
|
39
|
+
ActivationFlowLoading: () => ActivationFlowLoading2,
|
|
40
|
+
ActivationFlowRetrying: () => ActivationFlowRetrying2,
|
|
41
|
+
ActivationFlowRoot: () => ActivationFlowRoot2,
|
|
42
|
+
ActivationFlowSummary: () => ActivationFlowSummary2,
|
|
43
|
+
AmountPicker: () => AmountPicker,
|
|
44
|
+
AmountPickerConfirm: () => AmountPickerConfirm2,
|
|
45
|
+
AmountPickerCustom: () => AmountPickerCustom2,
|
|
46
|
+
AmountPickerOption: () => AmountPickerOption2,
|
|
47
|
+
AmountPickerRoot: () => AmountPickerRoot2,
|
|
48
|
+
BalanceBadge: () => BalanceBadge,
|
|
49
|
+
CancelPlanButton: () => CancelPlanButton,
|
|
50
|
+
CancelledPlanNotice: () => CancelledPlanNotice,
|
|
51
|
+
CancelledPlanNoticeAccessUntil: () => CancelledPlanNoticeAccessUntil2,
|
|
52
|
+
CancelledPlanNoticeCancelledOn: () => CancelledPlanNoticeCancelledOn2,
|
|
53
|
+
CancelledPlanNoticeDaysRemaining: () => CancelledPlanNoticeDaysRemaining2,
|
|
54
|
+
CancelledPlanNoticeExpires: () => CancelledPlanNoticeExpires2,
|
|
55
|
+
CancelledPlanNoticeHeading: () => CancelledPlanNoticeHeading2,
|
|
56
|
+
CancelledPlanNoticeReactivateButton: () => CancelledPlanNoticeReactivateButton2,
|
|
57
|
+
CancelledPlanNoticeReason: () => CancelledPlanNoticeReason2,
|
|
58
|
+
CancelledPlanNoticeRoot: () => CancelledPlanNoticeRoot2,
|
|
59
|
+
CheckoutSummary: () => CheckoutSummary,
|
|
60
|
+
CheckoutSummaryPlan: () => CheckoutSummaryPlan2,
|
|
61
|
+
CheckoutSummaryPrice: () => CheckoutSummaryPrice2,
|
|
62
|
+
CheckoutSummaryProduct: () => CheckoutSummaryProduct2,
|
|
63
|
+
CheckoutSummaryRoot: () => CheckoutSummaryRoot2,
|
|
64
|
+
CheckoutSummaryTaxNote: () => CheckoutSummaryTaxNote2,
|
|
65
|
+
CheckoutSummaryTrial: () => CheckoutSummaryTrial2,
|
|
66
|
+
CreditGate: () => CreditGate,
|
|
67
|
+
CreditGateError: () => CreditGateError2,
|
|
68
|
+
CreditGateHeading: () => CreditGateHeading2,
|
|
69
|
+
CreditGateLoading: () => CreditGateLoading2,
|
|
70
|
+
CreditGateRoot: () => CreditGateRoot2,
|
|
71
|
+
CreditGateSubheading: () => CreditGateSubheading2,
|
|
72
|
+
CreditGateTopup: () => CreditGateTopup,
|
|
73
|
+
MandateText: () => MandateText,
|
|
74
|
+
PaymentForm: () => PaymentForm,
|
|
75
|
+
PaymentFormCardElement: () => PaymentFormCardElement,
|
|
76
|
+
PaymentFormCustomerFields: () => PaymentFormCustomerFields2,
|
|
77
|
+
PaymentFormError: () => PaymentFormError2,
|
|
78
|
+
PaymentFormLoading: () => PaymentFormLoading2,
|
|
79
|
+
PaymentFormMandateText: () => PaymentFormMandateText,
|
|
80
|
+
PaymentFormPaymentElement: () => PaymentFormPaymentElement,
|
|
81
|
+
PaymentFormRoot: () => PaymentFormRoot2,
|
|
82
|
+
PaymentFormSubmitButton: () => PaymentFormSubmitButton2,
|
|
83
|
+
PaymentFormSummary: () => PaymentFormSummary,
|
|
84
|
+
PaymentFormTermsCheckbox: () => PaymentFormTermsCheckbox2,
|
|
85
|
+
PlanBadge: () => PlanBadge,
|
|
86
|
+
PlanSelector: () => PlanSelector,
|
|
87
|
+
PlanSelectorCard: () => PlanSelectorCard2,
|
|
88
|
+
PlanSelectorCardBadge: () => PlanSelectorCardBadge2,
|
|
89
|
+
PlanSelectorCardInterval: () => PlanSelectorCardInterval2,
|
|
90
|
+
PlanSelectorCardName: () => PlanSelectorCardName2,
|
|
91
|
+
PlanSelectorCardPrice: () => PlanSelectorCardPrice2,
|
|
92
|
+
PlanSelectorError: () => PlanSelectorError2,
|
|
93
|
+
PlanSelectorGrid: () => PlanSelectorGrid2,
|
|
94
|
+
PlanSelectorHeading: () => PlanSelectorHeading2,
|
|
95
|
+
PlanSelectorLoading: () => PlanSelectorLoading2,
|
|
96
|
+
PlanSelectorRoot: () => PlanSelectorRoot2,
|
|
97
|
+
ProductBadge: () => ProductBadge2,
|
|
98
|
+
PurchaseGate: () => PurchaseGate,
|
|
99
|
+
PurchaseGateAllowed: () => PurchaseGateAllowed2,
|
|
100
|
+
PurchaseGateBlocked: () => PurchaseGateBlocked2,
|
|
101
|
+
PurchaseGateError: () => PurchaseGateError2,
|
|
102
|
+
PurchaseGateLoading: () => PurchaseGateLoading2,
|
|
103
|
+
PurchaseGateRoot: () => PurchaseGateRoot2,
|
|
104
|
+
Slot: () => Slot,
|
|
105
|
+
Slottable: () => Slottable,
|
|
106
|
+
TopupForm: () => TopupForm,
|
|
107
|
+
TopupFormError: () => TopupFormError2,
|
|
108
|
+
TopupFormLoading: () => TopupFormLoading2,
|
|
109
|
+
TopupFormPaymentElement: () => TopupFormPaymentElement,
|
|
110
|
+
TopupFormRoot: () => TopupFormRoot2,
|
|
111
|
+
TopupFormSubmitButton: () => TopupFormSubmitButton2,
|
|
112
|
+
composeEventHandlers: () => composeEventHandlers,
|
|
113
|
+
composeRefs: () => composeRefs,
|
|
114
|
+
setRef: () => setRef,
|
|
115
|
+
useActivationFlow: () => useActivationFlow,
|
|
116
|
+
useAmountPicker: () => useAmountPicker,
|
|
117
|
+
useAmountPickerCopy: () => useAmountPickerCopy,
|
|
118
|
+
useCancelledPlanNotice: () => useCancelledPlanNotice,
|
|
119
|
+
useCheckoutSummary: () => useCheckoutSummary,
|
|
120
|
+
useCreditGate: () => useCreditGate,
|
|
121
|
+
usePlanSelector: () => usePlanSelector,
|
|
122
|
+
usePurchaseGate: () => usePurchaseGate,
|
|
123
|
+
useTopupForm: () => useTopupForm
|
|
124
|
+
});
|
|
125
|
+
module.exports = __toCommonJS(primitives_exports);
|
|
126
|
+
|
|
127
|
+
// src/primitives/slot.tsx
|
|
128
|
+
var import_react = __toESM(require("react"), 1);
|
|
129
|
+
|
|
130
|
+
// src/primitives/composeRefs.ts
|
|
131
|
+
function setRef(ref, value) {
|
|
132
|
+
if (typeof ref === "function") {
|
|
133
|
+
ref(value);
|
|
134
|
+
} else if (ref !== null && ref !== void 0) {
|
|
135
|
+
;
|
|
136
|
+
ref.current = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function composeRefs(...refs) {
|
|
140
|
+
return (node) => {
|
|
141
|
+
for (const ref of refs) {
|
|
142
|
+
setRef(ref, node);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/primitives/slot.tsx
|
|
148
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
149
|
+
var Slot = import_react.default.forwardRef((props, forwardedRef) => {
|
|
150
|
+
const { children, ...slotProps } = props;
|
|
151
|
+
const childrenArray = import_react.default.Children.toArray(children);
|
|
152
|
+
const slottable = childrenArray.find(isSlottable);
|
|
153
|
+
if (slottable) {
|
|
154
|
+
const newElement = slottable.props.children;
|
|
155
|
+
const newChildren = childrenArray.map((child) => {
|
|
156
|
+
if (child === slottable) {
|
|
157
|
+
if (import_react.default.Children.count(newElement) > 1) return import_react.default.Children.only(null);
|
|
158
|
+
return import_react.default.isValidElement(newElement) ? newElement.props.children : null;
|
|
159
|
+
}
|
|
160
|
+
return child;
|
|
161
|
+
});
|
|
162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children: import_react.default.isValidElement(newElement) ? import_react.default.cloneElement(newElement, void 0, newChildren) : null });
|
|
163
|
+
}
|
|
164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlotClone, { ...slotProps, ref: forwardedRef, children });
|
|
165
|
+
});
|
|
166
|
+
Slot.displayName = "Slot";
|
|
167
|
+
var SlotClone = import_react.default.forwardRef(
|
|
168
|
+
(props, forwardedRef) => {
|
|
169
|
+
const { children, ...slotProps } = props;
|
|
170
|
+
if (import_react.default.isValidElement(children)) {
|
|
171
|
+
const childProps = children.props ?? {};
|
|
172
|
+
const childRef = getElementRef(children);
|
|
173
|
+
const props2 = mergeProps(slotProps, childProps);
|
|
174
|
+
const cloneProps = { ...props2 };
|
|
175
|
+
if (forwardedRef) {
|
|
176
|
+
cloneProps.ref = childRef ? composeRefs(forwardedRef, childRef) : forwardedRef;
|
|
177
|
+
} else {
|
|
178
|
+
cloneProps.ref = childRef;
|
|
179
|
+
}
|
|
180
|
+
return import_react.default.cloneElement(children, cloneProps);
|
|
181
|
+
}
|
|
182
|
+
return import_react.default.Children.count(children) > 1 ? import_react.default.Children.only(null) : null;
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
SlotClone.displayName = "SlotClone";
|
|
186
|
+
var Slottable = ({ children }) => {
|
|
187
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
188
|
+
};
|
|
189
|
+
function isSlottable(child) {
|
|
190
|
+
return import_react.default.isValidElement(child) && child.type === Slottable;
|
|
191
|
+
}
|
|
192
|
+
function mergeProps(slotProps, childProps) {
|
|
193
|
+
const overrideProps = { ...childProps };
|
|
194
|
+
for (const propName in slotProps) {
|
|
195
|
+
const slotPropValue = slotProps[propName];
|
|
196
|
+
const childPropValue = childProps[propName];
|
|
197
|
+
const isHandler = /^on[A-Z]/.test(propName);
|
|
198
|
+
if (isHandler) {
|
|
199
|
+
if (slotPropValue && childPropValue) {
|
|
200
|
+
overrideProps[propName] = (...args) => {
|
|
201
|
+
;
|
|
202
|
+
childPropValue(...args);
|
|
203
|
+
const first = args[0];
|
|
204
|
+
if (!first?.defaultPrevented) {
|
|
205
|
+
;
|
|
206
|
+
slotPropValue(...args);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
} else if (slotPropValue) {
|
|
210
|
+
overrideProps[propName] = slotPropValue;
|
|
211
|
+
}
|
|
212
|
+
} else if (propName === "style") {
|
|
213
|
+
overrideProps[propName] = {
|
|
214
|
+
...slotPropValue,
|
|
215
|
+
...childPropValue
|
|
216
|
+
};
|
|
217
|
+
} else if (propName === "className") {
|
|
218
|
+
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
|
|
219
|
+
} else if (propName === "ref") {
|
|
220
|
+
} else if (childPropValue === void 0) {
|
|
221
|
+
overrideProps[propName] = slotPropValue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return overrideProps;
|
|
225
|
+
}
|
|
226
|
+
function getElementRef(element) {
|
|
227
|
+
const el = element;
|
|
228
|
+
return el.props?.ref ?? el.ref ?? null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/primitives/composeEventHandlers.ts
|
|
232
|
+
function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) {
|
|
233
|
+
return function handleEvent(event) {
|
|
234
|
+
originalEventHandler?.(event);
|
|
235
|
+
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
|
|
236
|
+
ourEventHandler?.(event);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/primitives/CheckoutSummary.tsx
|
|
242
|
+
var import_react9 = require("react");
|
|
243
|
+
|
|
244
|
+
// src/hooks/usePlan.ts
|
|
245
|
+
var import_react6 = require("react");
|
|
246
|
+
|
|
247
|
+
// src/hooks/useSolvaPay.ts
|
|
248
|
+
var import_react4 = require("react");
|
|
249
|
+
|
|
250
|
+
// src/SolvaPayProvider.tsx
|
|
251
|
+
var import_react3 = require("react");
|
|
252
|
+
|
|
253
|
+
// src/adapters/auth.ts
|
|
254
|
+
var defaultAuthAdapter = {
|
|
255
|
+
async getToken() {
|
|
256
|
+
if (typeof window === "undefined") return null;
|
|
257
|
+
const token = localStorage.getItem("auth_token");
|
|
258
|
+
return token || null;
|
|
259
|
+
},
|
|
260
|
+
async getUserId() {
|
|
261
|
+
const token = await this.getToken();
|
|
262
|
+
if (!token) return null;
|
|
263
|
+
try {
|
|
264
|
+
const parts = token.split(".");
|
|
265
|
+
if (parts.length === 3) {
|
|
266
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
267
|
+
return payload.sub || payload.user_id || null;
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/utils/headers.ts
|
|
276
|
+
var CUSTOMER_REF_KEY = "solvapay_customerRef";
|
|
277
|
+
var CUSTOMER_REF_EXPIRY = "solvapay_customerRef_expiry";
|
|
278
|
+
var CUSTOMER_REF_USER_ID_KEY = "solvapay_customerRef_userId";
|
|
279
|
+
function getAuthAdapter(config) {
|
|
280
|
+
if (config?.auth?.adapter) {
|
|
281
|
+
return config.auth.adapter;
|
|
282
|
+
}
|
|
283
|
+
if (config?.auth?.getToken || config?.auth?.getUserId) {
|
|
284
|
+
return {
|
|
285
|
+
async getToken() {
|
|
286
|
+
return config?.auth?.getToken?.() || null;
|
|
287
|
+
},
|
|
288
|
+
async getUserId() {
|
|
289
|
+
return config?.auth?.getUserId?.() || null;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return defaultAuthAdapter;
|
|
294
|
+
}
|
|
295
|
+
function getCachedCustomerRef(userId) {
|
|
296
|
+
if (typeof window === "undefined") return null;
|
|
297
|
+
const cached = localStorage.getItem(CUSTOMER_REF_KEY);
|
|
298
|
+
const expiry = localStorage.getItem(CUSTOMER_REF_EXPIRY);
|
|
299
|
+
const cachedUserId = localStorage.getItem(CUSTOMER_REF_USER_ID_KEY);
|
|
300
|
+
if (!cached || !expiry) return null;
|
|
301
|
+
if (Date.now() > parseInt(expiry)) {
|
|
302
|
+
localStorage.removeItem(CUSTOMER_REF_KEY);
|
|
303
|
+
localStorage.removeItem(CUSTOMER_REF_EXPIRY);
|
|
304
|
+
localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
if (userId !== void 0 && userId !== null) {
|
|
308
|
+
if (cachedUserId !== userId) {
|
|
309
|
+
clearCachedCustomerRef();
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return cached;
|
|
314
|
+
}
|
|
315
|
+
var CACHE_DURATION = 24 * 60 * 60 * 1e3;
|
|
316
|
+
function clearCachedCustomerRef() {
|
|
317
|
+
if (typeof window === "undefined") return;
|
|
318
|
+
localStorage.removeItem(CUSTOMER_REF_KEY);
|
|
319
|
+
localStorage.removeItem(CUSTOMER_REF_EXPIRY);
|
|
320
|
+
localStorage.removeItem(CUSTOMER_REF_USER_ID_KEY);
|
|
321
|
+
}
|
|
322
|
+
async function buildRequestHeaders(config) {
|
|
323
|
+
const adapter = getAuthAdapter(config);
|
|
324
|
+
const token = await adapter.getToken();
|
|
325
|
+
const userId = await adapter.getUserId();
|
|
326
|
+
const cachedRef = getCachedCustomerRef(userId);
|
|
327
|
+
const headers = {
|
|
328
|
+
"Content-Type": "application/json"
|
|
329
|
+
};
|
|
330
|
+
if (token) {
|
|
331
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
332
|
+
}
|
|
333
|
+
if (cachedRef) {
|
|
334
|
+
headers["x-solvapay-customer-ref"] = cachedRef;
|
|
335
|
+
}
|
|
336
|
+
if (config?.headers) {
|
|
337
|
+
const custom = typeof config.headers === "function" ? await config.headers() : config.headers;
|
|
338
|
+
Object.assign(headers, custom);
|
|
339
|
+
}
|
|
340
|
+
return { headers, userId };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/i18n/context.tsx
|
|
344
|
+
var import_react2 = require("react");
|
|
345
|
+
|
|
346
|
+
// src/i18n/en.ts
|
|
347
|
+
function intervalPhrase(ctx) {
|
|
348
|
+
const interval = ctx.plan?.interval;
|
|
349
|
+
const count = ctx.plan?.intervalCount ?? 1;
|
|
350
|
+
if (!interval) return "";
|
|
351
|
+
return count > 1 ? `${count} ${interval}s` : interval;
|
|
352
|
+
}
|
|
353
|
+
function trialPhrase(ctx) {
|
|
354
|
+
const trialDays = ctx.plan?.trialDays ?? ctx.trialDays;
|
|
355
|
+
return trialDays ? ` after your ${trialDays}-day free trial` : "";
|
|
356
|
+
}
|
|
357
|
+
function termsSentence(ctx) {
|
|
358
|
+
const { termsUrl, privacyUrl } = ctx.merchant;
|
|
359
|
+
if (termsUrl && privacyUrl) {
|
|
360
|
+
return ` See ${termsUrl} and ${privacyUrl}.`;
|
|
361
|
+
}
|
|
362
|
+
if (termsUrl) return ` See ${termsUrl}.`;
|
|
363
|
+
if (privacyUrl) return ` See ${privacyUrl}.`;
|
|
364
|
+
return "";
|
|
365
|
+
}
|
|
366
|
+
var enCopy = {
|
|
367
|
+
mandate: {
|
|
368
|
+
recurring: (ctx) => {
|
|
369
|
+
const period = intervalPhrase(ctx);
|
|
370
|
+
const trial = trialPhrase(ctx);
|
|
371
|
+
const every = period ? ` every ${period}` : "";
|
|
372
|
+
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)}`;
|
|
373
|
+
},
|
|
374
|
+
oneTime: (ctx) => {
|
|
375
|
+
const product = ctx.product?.name ? ` for ${ctx.product.name}` : "";
|
|
376
|
+
return `By confirming, you authorize ${ctx.merchant.legalName} to charge ${ctx.amountFormatted}${product}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
377
|
+
},
|
|
378
|
+
topup: (ctx) => {
|
|
379
|
+
const product = ctx.product?.name ? ` to add credits to your ${ctx.product.name} balance` : " to add credits to your balance";
|
|
380
|
+
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)}`;
|
|
381
|
+
},
|
|
382
|
+
usageMetered: (ctx) => {
|
|
383
|
+
const measures = ctx.plan?.measures ?? "unit";
|
|
384
|
+
const cycle = ctx.plan?.billingCycle ?? "monthly";
|
|
385
|
+
const product = ctx.product?.name ?? "the service";
|
|
386
|
+
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)}`;
|
|
387
|
+
},
|
|
388
|
+
freeTier: (ctx) => {
|
|
389
|
+
const product = ctx.product?.name ?? "this plan";
|
|
390
|
+
const planPhrase = ctx.plan?.name ? ` on ${ctx.plan.name}` : "";
|
|
391
|
+
return `By confirming, you activate ${product}${planPhrase}. Payments are processed by SolvaPay.${termsSentence(ctx)}`;
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
cta: {
|
|
395
|
+
payNow: "Pay Now",
|
|
396
|
+
topUp: "Top Up",
|
|
397
|
+
subscribe: "Subscribe",
|
|
398
|
+
trialStart: "Start {trialDays}-day free trial",
|
|
399
|
+
payAmount: "Pay {amount}",
|
|
400
|
+
addAmount: "Add {amount}",
|
|
401
|
+
startUsing: "Start using {product}",
|
|
402
|
+
processing: "Processing..."
|
|
403
|
+
},
|
|
404
|
+
interval: {
|
|
405
|
+
day: "day",
|
|
406
|
+
week: "week",
|
|
407
|
+
month: "month",
|
|
408
|
+
year: "year",
|
|
409
|
+
every: "every {n} {unit}",
|
|
410
|
+
free: "Free",
|
|
411
|
+
trial: "{trialDays}-day free trial"
|
|
412
|
+
},
|
|
413
|
+
terms: {
|
|
414
|
+
checkboxLabel: "I agree to the terms and privacy policy"
|
|
415
|
+
},
|
|
416
|
+
customer: {
|
|
417
|
+
chargingTo: "Charging to {email}",
|
|
418
|
+
emailLabel: "Email",
|
|
419
|
+
nameLabel: "Name"
|
|
420
|
+
},
|
|
421
|
+
balance: {
|
|
422
|
+
credits: " credits",
|
|
423
|
+
currencyEquivalent: " (~{amount})"
|
|
424
|
+
},
|
|
425
|
+
product: {
|
|
426
|
+
currentProductLabel: "Current product: {name}"
|
|
427
|
+
},
|
|
428
|
+
topup: {
|
|
429
|
+
selectOrEnterAmount: "Please select or enter an amount",
|
|
430
|
+
minAmount: "Minimum amount is {amount}",
|
|
431
|
+
maxAmount: "Maximum amount is {amount}"
|
|
432
|
+
},
|
|
433
|
+
activation: {
|
|
434
|
+
paymentRequired: "This plan requires payment. Please select a different plan.",
|
|
435
|
+
invalidConfiguration: "Invalid plan configuration.",
|
|
436
|
+
unexpectedResponse: "Unexpected response from server.",
|
|
437
|
+
failed: "Activation failed"
|
|
438
|
+
},
|
|
439
|
+
planSelector: {
|
|
440
|
+
heading: "Choose your pricing",
|
|
441
|
+
currentBadge: "Current",
|
|
442
|
+
popularBadge: "Popular",
|
|
443
|
+
freeBadge: "Free",
|
|
444
|
+
perIntervalShort: "/{interval}",
|
|
445
|
+
continueButton: "Continue",
|
|
446
|
+
backButton: "\u2190 Back to plans",
|
|
447
|
+
trialBadge: "{trialDays}-day free trial"
|
|
448
|
+
},
|
|
449
|
+
amountPicker: {
|
|
450
|
+
selectAmountLabel: "Select an amount",
|
|
451
|
+
customAmountLabel: "Or enter a custom amount",
|
|
452
|
+
creditEstimateExact: "= {credits} credits",
|
|
453
|
+
creditEstimateApprox: "~ {credits} credits"
|
|
454
|
+
},
|
|
455
|
+
activationFlow: {
|
|
456
|
+
heading: "Confirm your plan",
|
|
457
|
+
activateButton: "Activate",
|
|
458
|
+
activatingLabel: "Activating...",
|
|
459
|
+
topupHeading: "Add credits",
|
|
460
|
+
topupSubheading: "Top up your credits to activate this plan.",
|
|
461
|
+
continueToPayment: "Continue to payment",
|
|
462
|
+
changeAmountButton: "Change amount",
|
|
463
|
+
retryingHeading: "Activating your plan...",
|
|
464
|
+
retryingSubheading: "Payment received. Setting up your plan.",
|
|
465
|
+
activatedHeading: "Plan selected",
|
|
466
|
+
activatedSubheading: "Your plan is now active.",
|
|
467
|
+
tryAgainButton: "Try Again",
|
|
468
|
+
backButton: "\u2190 Back to plan selection"
|
|
469
|
+
},
|
|
470
|
+
cancelPlan: {
|
|
471
|
+
button: "Cancel plan",
|
|
472
|
+
buttonLoading: "Cancelling...",
|
|
473
|
+
confirmRecurring: "Are you sure you want to cancel your subscription?",
|
|
474
|
+
confirmUsageBased: "Are you sure you want to deactivate your plan? This will take effect immediately."
|
|
475
|
+
},
|
|
476
|
+
cancelledNotice: {
|
|
477
|
+
heading: "Your purchase has been cancelled",
|
|
478
|
+
expiresLabel: "Purchase Expires: {date}",
|
|
479
|
+
daysRemaining: "{days} days remaining",
|
|
480
|
+
dayRemaining: "1 day remaining",
|
|
481
|
+
accessUntil: "You'll continue to have access to {product} features until this date",
|
|
482
|
+
accessEnded: "Your purchase access has ended",
|
|
483
|
+
cancelledOn: "Cancelled on {date}",
|
|
484
|
+
reactivateButton: "Undo Cancellation",
|
|
485
|
+
reactivateButtonLoading: "Reactivating..."
|
|
486
|
+
},
|
|
487
|
+
creditGate: {
|
|
488
|
+
lowBalanceHeading: "You're out of credits",
|
|
489
|
+
lowBalanceSubheading: "Top up to continue using {product}",
|
|
490
|
+
topUpCta: "Top up now"
|
|
491
|
+
},
|
|
492
|
+
errors: {
|
|
493
|
+
paymentInitFailed: "Payment initialization failed",
|
|
494
|
+
topupInitFailed: "Top-up initialization failed",
|
|
495
|
+
configMissingPlanOrProduct: "PaymentForm: either planRef or productRef is required",
|
|
496
|
+
configMissingAmount: "TopupForm: amount must be a positive number",
|
|
497
|
+
unknownError: "Unknown error",
|
|
498
|
+
stripeUnavailable: "Stripe is not available. Please refresh the page.",
|
|
499
|
+
paymentIntentUnavailable: "Payment intent not available. Please refresh the page.",
|
|
500
|
+
cardElementMissing: "Card element not found",
|
|
501
|
+
paymentUnexpected: "An unexpected error occurred.",
|
|
502
|
+
paymentProcessingFailed: "Payment processing failed. Please try again or contact support.",
|
|
503
|
+
paymentRequires3ds: "Payment requires additional authentication. Please complete the verification.",
|
|
504
|
+
paymentProcessingTimeout: "Payment processing timed out \u2014 webhooks may not be configured",
|
|
505
|
+
paymentStatusPrefix: "Payment status: {status}"
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// src/i18n/context.tsx
|
|
510
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
511
|
+
var CopyContext = (0, import_react2.createContext)({
|
|
512
|
+
locale: void 0,
|
|
513
|
+
copy: enCopy
|
|
514
|
+
});
|
|
515
|
+
function useCopyContext() {
|
|
516
|
+
return (0, import_react2.useContext)(CopyContext);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/SolvaPayProvider.tsx
|
|
520
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
521
|
+
var SolvaPayContext = (0, import_react3.createContext)(null);
|
|
522
|
+
|
|
523
|
+
// src/hooks/useSolvaPay.ts
|
|
524
|
+
function useSolvaPay() {
|
|
525
|
+
const context = (0, import_react4.useContext)(SolvaPayContext);
|
|
526
|
+
if (!context) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
"useSolvaPay must be used within a SolvaPayProvider. Wrap your component tree with <SolvaPayProvider> to use this hook."
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
return context;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/hooks/usePlans.ts
|
|
535
|
+
var import_react5 = require("react");
|
|
536
|
+
var plansCache = /* @__PURE__ */ new Map();
|
|
537
|
+
var CACHE_DURATION2 = 5 * 60 * 1e3;
|
|
538
|
+
function processPlans(raw, filter, sortBy) {
|
|
539
|
+
let result = sortBy ? [...raw].sort(sortBy) : raw;
|
|
540
|
+
if (filter) result = result.filter(filter);
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
function computeInitialIndex(plans, initialPlanRef, autoSelectFirstPaid) {
|
|
544
|
+
if (plans.length === 0) return 0;
|
|
545
|
+
if (initialPlanRef) {
|
|
546
|
+
const idx = plans.findIndex((p) => p.reference === initialPlanRef);
|
|
547
|
+
if (idx >= 0) return idx;
|
|
548
|
+
}
|
|
549
|
+
if (autoSelectFirstPaid) {
|
|
550
|
+
const idx = plans.findIndex((p) => p.requiresPayment !== false);
|
|
551
|
+
return idx >= 0 ? idx : 0;
|
|
552
|
+
}
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
function usePlans(options) {
|
|
556
|
+
const {
|
|
557
|
+
fetcher,
|
|
558
|
+
productRef,
|
|
559
|
+
filter,
|
|
560
|
+
sortBy,
|
|
561
|
+
autoSelectFirstPaid = false,
|
|
562
|
+
initialPlanRef,
|
|
563
|
+
selectionReady = true
|
|
564
|
+
} = options;
|
|
565
|
+
const fetcherRef = (0, import_react5.useRef)(fetcher);
|
|
566
|
+
const filterRef = (0, import_react5.useRef)(filter);
|
|
567
|
+
const sortByRef = (0, import_react5.useRef)(sortBy);
|
|
568
|
+
const autoSelectFirstPaidRef = (0, import_react5.useRef)(autoSelectFirstPaid);
|
|
569
|
+
const initialPlanRefRef = (0, import_react5.useRef)(initialPlanRef);
|
|
570
|
+
const selectionReadyRef = (0, import_react5.useRef)(selectionReady);
|
|
571
|
+
const hasAppliedInitialRef = (0, import_react5.useRef)(false);
|
|
572
|
+
const userHasSelectedRef = (0, import_react5.useRef)(false);
|
|
573
|
+
const [selectedPlanIndex, setSelectedPlanIndexState] = (0, import_react5.useState)(() => {
|
|
574
|
+
if (!selectionReady || !productRef) return 0;
|
|
575
|
+
const cached = plansCache.get(productRef);
|
|
576
|
+
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
const processed = processPlans(cached.plans, filter, sortBy);
|
|
580
|
+
if (processed.length === 0) return 0;
|
|
581
|
+
const idx = computeInitialIndex(processed, initialPlanRef, autoSelectFirstPaid);
|
|
582
|
+
hasAppliedInitialRef.current = true;
|
|
583
|
+
return idx;
|
|
584
|
+
});
|
|
585
|
+
const [plans, setPlans] = (0, import_react5.useState)(() => {
|
|
586
|
+
if (!productRef) return [];
|
|
587
|
+
const cached = plansCache.get(productRef);
|
|
588
|
+
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2 || cached.plans.length === 0) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
return processPlans(cached.plans, filter, sortBy);
|
|
592
|
+
});
|
|
593
|
+
const [loading, setLoading] = (0, import_react5.useState)(() => plans.length === 0);
|
|
594
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
595
|
+
(0, import_react5.useEffect)(() => {
|
|
596
|
+
fetcherRef.current = fetcher;
|
|
597
|
+
}, [fetcher]);
|
|
598
|
+
(0, import_react5.useEffect)(() => {
|
|
599
|
+
filterRef.current = filter;
|
|
600
|
+
}, [filter]);
|
|
601
|
+
(0, import_react5.useEffect)(() => {
|
|
602
|
+
sortByRef.current = sortBy;
|
|
603
|
+
}, [sortBy]);
|
|
604
|
+
(0, import_react5.useEffect)(() => {
|
|
605
|
+
autoSelectFirstPaidRef.current = autoSelectFirstPaid;
|
|
606
|
+
}, [autoSelectFirstPaid]);
|
|
607
|
+
(0, import_react5.useEffect)(() => {
|
|
608
|
+
initialPlanRefRef.current = initialPlanRef;
|
|
609
|
+
}, [initialPlanRef]);
|
|
610
|
+
(0, import_react5.useEffect)(() => {
|
|
611
|
+
selectionReadyRef.current = selectionReady;
|
|
612
|
+
}, [selectionReady]);
|
|
613
|
+
const setSelectedPlanIndex = (0, import_react5.useCallback)((index) => {
|
|
614
|
+
userHasSelectedRef.current = true;
|
|
615
|
+
setSelectedPlanIndexState(index);
|
|
616
|
+
}, []);
|
|
617
|
+
const applyInitialSelection = (0, import_react5.useCallback)((processedPlans) => {
|
|
618
|
+
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
619
|
+
if (!selectionReadyRef.current || processedPlans.length === 0) return;
|
|
620
|
+
hasAppliedInitialRef.current = true;
|
|
621
|
+
const idx = computeInitialIndex(
|
|
622
|
+
processedPlans,
|
|
623
|
+
initialPlanRefRef.current,
|
|
624
|
+
autoSelectFirstPaidRef.current
|
|
625
|
+
);
|
|
626
|
+
setSelectedPlanIndexState(idx);
|
|
627
|
+
}, []);
|
|
628
|
+
const fetchPlans = (0, import_react5.useCallback)(
|
|
629
|
+
async (force = false) => {
|
|
630
|
+
if (!productRef) {
|
|
631
|
+
setError(new Error("Product reference not configured"));
|
|
632
|
+
setLoading(false);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const cached = plansCache.get(productRef);
|
|
636
|
+
const now = Date.now();
|
|
637
|
+
if (!force && cached && now - cached.timestamp < CACHE_DURATION2) {
|
|
638
|
+
const processedPlans = processPlans(
|
|
639
|
+
cached.plans,
|
|
640
|
+
filterRef.current,
|
|
641
|
+
sortByRef.current
|
|
642
|
+
);
|
|
643
|
+
setPlans(processedPlans);
|
|
644
|
+
setLoading(false);
|
|
645
|
+
setError(null);
|
|
646
|
+
applyInitialSelection(processedPlans);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (cached?.promise) {
|
|
650
|
+
try {
|
|
651
|
+
setLoading(true);
|
|
652
|
+
const fetchedPlans = await cached.promise;
|
|
653
|
+
const processedPlans = processPlans(
|
|
654
|
+
fetchedPlans,
|
|
655
|
+
filterRef.current,
|
|
656
|
+
sortByRef.current
|
|
657
|
+
);
|
|
658
|
+
setPlans(processedPlans);
|
|
659
|
+
setError(null);
|
|
660
|
+
applyInitialSelection(processedPlans);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
663
|
+
} finally {
|
|
664
|
+
setLoading(false);
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
setLoading(true);
|
|
670
|
+
setError(null);
|
|
671
|
+
const fetchPromise = fetcherRef.current(productRef);
|
|
672
|
+
plansCache.set(productRef, { plans: [], timestamp: now, promise: fetchPromise });
|
|
673
|
+
const fetchedPlans = await fetchPromise;
|
|
674
|
+
plansCache.set(productRef, { plans: fetchedPlans, timestamp: now, promise: null });
|
|
675
|
+
const processedPlans = processPlans(
|
|
676
|
+
fetchedPlans,
|
|
677
|
+
filterRef.current,
|
|
678
|
+
sortByRef.current
|
|
679
|
+
);
|
|
680
|
+
setPlans(processedPlans);
|
|
681
|
+
applyInitialSelection(processedPlans);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
plansCache.delete(productRef);
|
|
684
|
+
setError(err instanceof Error ? err : new Error("Failed to load plans"));
|
|
685
|
+
} finally {
|
|
686
|
+
setLoading(false);
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
[productRef, applyInitialSelection]
|
|
690
|
+
);
|
|
691
|
+
(0, import_react5.useEffect)(() => {
|
|
692
|
+
fetchPlans();
|
|
693
|
+
}, [fetchPlans]);
|
|
694
|
+
(0, import_react5.useEffect)(() => {
|
|
695
|
+
if (hasAppliedInitialRef.current || userHasSelectedRef.current) return;
|
|
696
|
+
if (!selectionReady || plans.length === 0) return;
|
|
697
|
+
applyInitialSelection(plans);
|
|
698
|
+
}, [selectionReady, plans, applyInitialSelection]);
|
|
699
|
+
const selectedPlan = (0, import_react5.useMemo)(() => plans[selectedPlanIndex] || null, [plans, selectedPlanIndex]);
|
|
700
|
+
const selectPlan = (0, import_react5.useCallback)(
|
|
701
|
+
(planRef) => {
|
|
702
|
+
const index = plans.findIndex((p) => p.reference === planRef);
|
|
703
|
+
if (index >= 0) {
|
|
704
|
+
setSelectedPlanIndex(index);
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
[plans, setSelectedPlanIndex]
|
|
708
|
+
);
|
|
709
|
+
return {
|
|
710
|
+
plans,
|
|
711
|
+
loading,
|
|
712
|
+
error,
|
|
713
|
+
selectedPlanIndex,
|
|
714
|
+
selectedPlan,
|
|
715
|
+
setSelectedPlanIndex,
|
|
716
|
+
selectPlan,
|
|
717
|
+
refetch: () => fetchPlans(true),
|
|
718
|
+
isSelectionReady: hasAppliedInitialRef.current
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/hooks/usePlan.ts
|
|
723
|
+
async function listPlans(productRef, config) {
|
|
724
|
+
const base = config?.api?.listPlans || "/api/list-plans";
|
|
725
|
+
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
726
|
+
const fetchFn = config?.fetch || fetch;
|
|
727
|
+
const { headers } = await buildRequestHeaders(config);
|
|
728
|
+
const res = await fetchFn(url, { method: "GET", headers });
|
|
729
|
+
if (!res.ok) {
|
|
730
|
+
const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
|
|
731
|
+
config?.onError?.(error, "listPlans");
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
const data = await res.json();
|
|
735
|
+
return data.plans ?? [];
|
|
736
|
+
}
|
|
737
|
+
function usePlan(options) {
|
|
738
|
+
const { planRef, productRef } = options;
|
|
739
|
+
const { _config } = useSolvaPay();
|
|
740
|
+
const findPlan = (0, import_react6.useCallback)(
|
|
741
|
+
(plans) => {
|
|
742
|
+
if (!planRef) return null;
|
|
743
|
+
return plans.find((p) => p.reference === planRef) || null;
|
|
744
|
+
},
|
|
745
|
+
[planRef]
|
|
746
|
+
);
|
|
747
|
+
const [plan, setPlan] = (0, import_react6.useState)(() => {
|
|
748
|
+
if (!planRef || !productRef) return null;
|
|
749
|
+
const cached = plansCache.get(productRef);
|
|
750
|
+
if (!cached || Date.now() - cached.timestamp >= CACHE_DURATION2) return null;
|
|
751
|
+
return findPlan(cached.plans);
|
|
752
|
+
});
|
|
753
|
+
const [loading, setLoading] = (0, import_react6.useState)(() => !!(planRef && !plan));
|
|
754
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
755
|
+
const load = (0, import_react6.useCallback)(
|
|
756
|
+
async (force = false) => {
|
|
757
|
+
if (!planRef) {
|
|
758
|
+
setPlan(null);
|
|
759
|
+
setLoading(false);
|
|
760
|
+
setError(null);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!productRef) {
|
|
764
|
+
setLoading(false);
|
|
765
|
+
setError(
|
|
766
|
+
new Error("usePlan: productRef is required to resolve a plan reference")
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const cached = plansCache.get(productRef);
|
|
771
|
+
const now = Date.now();
|
|
772
|
+
if (!force && cached?.plans.length && now - cached.timestamp < CACHE_DURATION2) {
|
|
773
|
+
const p = findPlan(cached.plans);
|
|
774
|
+
setPlan(p);
|
|
775
|
+
setLoading(false);
|
|
776
|
+
setError(
|
|
777
|
+
p ? null : new Error(
|
|
778
|
+
`Plan "${planRef}" not found in product "${productRef}"`
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (!force && cached?.promise) {
|
|
784
|
+
try {
|
|
785
|
+
setLoading(true);
|
|
786
|
+
const plans = await cached.promise;
|
|
787
|
+
const p = findPlan(plans);
|
|
788
|
+
setPlan(p);
|
|
789
|
+
setError(
|
|
790
|
+
p ? null : new Error(
|
|
791
|
+
`Plan "${planRef}" not found in product "${productRef}"`
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
} catch (err) {
|
|
795
|
+
setError(err instanceof Error ? err : new Error("Failed to load plan"));
|
|
796
|
+
} finally {
|
|
797
|
+
setLoading(false);
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
setLoading(true);
|
|
803
|
+
setError(null);
|
|
804
|
+
const promise = listPlans(productRef, _config);
|
|
805
|
+
plansCache.set(productRef, { plans: [], timestamp: now, promise });
|
|
806
|
+
const plans = await promise;
|
|
807
|
+
plansCache.set(productRef, { plans, timestamp: now, promise: null });
|
|
808
|
+
const p = findPlan(plans);
|
|
809
|
+
setPlan(p);
|
|
810
|
+
if (!p) {
|
|
811
|
+
setError(new Error(`Plan "${planRef}" not found in product "${productRef}"`));
|
|
812
|
+
}
|
|
813
|
+
} catch (err) {
|
|
814
|
+
plansCache.delete(productRef);
|
|
815
|
+
setError(err instanceof Error ? err : new Error("Failed to load plan"));
|
|
816
|
+
} finally {
|
|
817
|
+
setLoading(false);
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
[planRef, productRef, _config, findPlan]
|
|
821
|
+
);
|
|
822
|
+
(0, import_react6.useEffect)(() => {
|
|
823
|
+
load();
|
|
824
|
+
}, [load]);
|
|
825
|
+
return {
|
|
826
|
+
plan,
|
|
827
|
+
loading,
|
|
828
|
+
error,
|
|
829
|
+
refetch: () => load(true)
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/hooks/useProduct.ts
|
|
834
|
+
var import_react7 = require("react");
|
|
835
|
+
var productCache = /* @__PURE__ */ new Map();
|
|
836
|
+
var CACHE_DURATION3 = 5 * 60 * 1e3;
|
|
837
|
+
function routeFor(config) {
|
|
838
|
+
return config?.api?.getProduct || "/api/get-product";
|
|
839
|
+
}
|
|
840
|
+
async function fetchProduct(productRef, config) {
|
|
841
|
+
const base = routeFor(config);
|
|
842
|
+
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
843
|
+
const fetchFn = config?.fetch || fetch;
|
|
844
|
+
const { headers } = await buildRequestHeaders(config);
|
|
845
|
+
const res = await fetchFn(url, { method: "GET", headers });
|
|
846
|
+
if (!res.ok) {
|
|
847
|
+
const error = new Error(`Failed to fetch product: ${res.statusText || res.status}`);
|
|
848
|
+
config?.onError?.(error, "getProduct");
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
return await res.json();
|
|
852
|
+
}
|
|
853
|
+
function useProduct(productRef) {
|
|
854
|
+
const { _config } = useSolvaPay();
|
|
855
|
+
const cacheKey = productRef || "";
|
|
856
|
+
const [product, setProduct] = (0, import_react7.useState)(
|
|
857
|
+
() => productRef ? productCache.get(cacheKey)?.product ?? null : null
|
|
858
|
+
);
|
|
859
|
+
const [loading, setLoading] = (0, import_react7.useState)(() => {
|
|
860
|
+
if (!productRef) return false;
|
|
861
|
+
const cached = productCache.get(cacheKey);
|
|
862
|
+
return !cached || !cached.product && !cached.promise;
|
|
863
|
+
});
|
|
864
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
865
|
+
const load = (0, import_react7.useCallback)(
|
|
866
|
+
async (force = false) => {
|
|
867
|
+
if (!productRef) {
|
|
868
|
+
setProduct(null);
|
|
869
|
+
setLoading(false);
|
|
870
|
+
setError(null);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const cached = productCache.get(productRef);
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
if (!force && cached?.product && now - cached.timestamp < CACHE_DURATION3) {
|
|
876
|
+
setProduct(cached.product);
|
|
877
|
+
setLoading(false);
|
|
878
|
+
setError(null);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!force && cached?.promise) {
|
|
882
|
+
try {
|
|
883
|
+
setLoading(true);
|
|
884
|
+
const p = await cached.promise;
|
|
885
|
+
setProduct(p);
|
|
886
|
+
setError(null);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
setError(err instanceof Error ? err : new Error("Failed to load product"));
|
|
889
|
+
} finally {
|
|
890
|
+
setLoading(false);
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
setLoading(true);
|
|
896
|
+
setError(null);
|
|
897
|
+
const promise = fetchProduct(productRef, _config);
|
|
898
|
+
productCache.set(productRef, { product: null, promise, timestamp: now });
|
|
899
|
+
const p = await promise;
|
|
900
|
+
productCache.set(productRef, { product: p, promise: null, timestamp: now });
|
|
901
|
+
setProduct(p);
|
|
902
|
+
} catch (err) {
|
|
903
|
+
productCache.delete(productRef);
|
|
904
|
+
setError(err instanceof Error ? err : new Error("Failed to load product"));
|
|
905
|
+
} finally {
|
|
906
|
+
setLoading(false);
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
[productRef, _config]
|
|
910
|
+
);
|
|
911
|
+
(0, import_react7.useEffect)(() => {
|
|
912
|
+
load();
|
|
913
|
+
}, [load]);
|
|
914
|
+
return {
|
|
915
|
+
product,
|
|
916
|
+
loading,
|
|
917
|
+
error,
|
|
918
|
+
refetch: () => load(true)
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/hooks/useCopy.ts
|
|
923
|
+
function useCopy() {
|
|
924
|
+
return useCopyContext().copy;
|
|
925
|
+
}
|
|
926
|
+
function useLocale() {
|
|
927
|
+
return useCopyContext().locale;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/utils/format.ts
|
|
931
|
+
var ZERO_DECIMAL_CURRENCIES = /* @__PURE__ */ new Set([
|
|
932
|
+
"bif",
|
|
933
|
+
"clp",
|
|
934
|
+
"djf",
|
|
935
|
+
"gnf",
|
|
936
|
+
"jpy",
|
|
937
|
+
"kmf",
|
|
938
|
+
"krw",
|
|
939
|
+
"mga",
|
|
940
|
+
"pyg",
|
|
941
|
+
"rwf",
|
|
942
|
+
"ugx",
|
|
943
|
+
"vnd",
|
|
944
|
+
"vuv",
|
|
945
|
+
"xaf",
|
|
946
|
+
"xof",
|
|
947
|
+
"xpf"
|
|
948
|
+
]);
|
|
949
|
+
function getFractionDigits(currency) {
|
|
950
|
+
return ZERO_DECIMAL_CURRENCIES.has(currency.toLowerCase()) ? 0 : 2;
|
|
951
|
+
}
|
|
952
|
+
function toMajorUnits(amountMinor, currency) {
|
|
953
|
+
const fractionDigits = getFractionDigits(currency);
|
|
954
|
+
return fractionDigits === 0 ? amountMinor : amountMinor / 100;
|
|
955
|
+
}
|
|
956
|
+
function formatPrice(amountMinor, currency, opts = {}) {
|
|
957
|
+
const { locale, interval, intervalCount = 1, free = "Free" } = opts;
|
|
958
|
+
if (amountMinor === 0 && free !== "") {
|
|
959
|
+
return free;
|
|
960
|
+
}
|
|
961
|
+
const fractionDigits = getFractionDigits(currency);
|
|
962
|
+
const major = toMajorUnits(amountMinor, currency);
|
|
963
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
964
|
+
style: "currency",
|
|
965
|
+
currency: currency.toUpperCase(),
|
|
966
|
+
minimumFractionDigits: fractionDigits,
|
|
967
|
+
maximumFractionDigits: fractionDigits
|
|
968
|
+
});
|
|
969
|
+
const formatted = formatter.format(major);
|
|
970
|
+
if (!interval) return formatted;
|
|
971
|
+
const suffix = intervalCount > 1 ? `${intervalCount} ${interval}s` : interval;
|
|
972
|
+
return `${formatted} / ${suffix}`;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/i18n/interpolate.ts
|
|
976
|
+
function interpolate(template, vars) {
|
|
977
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
978
|
+
const value = vars[key];
|
|
979
|
+
if (value === void 0 || value === null) return match;
|
|
980
|
+
return String(value);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/components/PlanSelectionContext.tsx
|
|
985
|
+
var import_react8 = require("react");
|
|
986
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
987
|
+
var PlanSelectionContext = (0, import_react8.createContext)(null);
|
|
988
|
+
var PlanSelectionProvider = ({
|
|
989
|
+
value,
|
|
990
|
+
children
|
|
991
|
+
}) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PlanSelectionContext.Provider, { value, children });
|
|
992
|
+
function usePlanSelection() {
|
|
993
|
+
return (0, import_react8.useContext)(PlanSelectionContext);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/utils/errors.ts
|
|
997
|
+
var DOCS_BASE_URL = "https://solvapay.com/docs";
|
|
998
|
+
var SolvaPayError = class extends Error {
|
|
999
|
+
code;
|
|
1000
|
+
docsUrl;
|
|
1001
|
+
constructor(code, message, docsUrl) {
|
|
1002
|
+
super(message);
|
|
1003
|
+
this.name = "SolvaPayError";
|
|
1004
|
+
this.code = code;
|
|
1005
|
+
this.docsUrl = docsUrl;
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
var MissingProviderError = class extends SolvaPayError {
|
|
1009
|
+
constructor(primitiveName) {
|
|
1010
|
+
const docsUrl = `${DOCS_BASE_URL}/troubleshooting/missing-provider`;
|
|
1011
|
+
super(
|
|
1012
|
+
"MISSING_PROVIDER",
|
|
1013
|
+
`${primitiveName} must be rendered inside <SolvaPayProvider>. Wrap your app (or the section using ${primitiveName}) with <SolvaPayProvider config={{ ... }}>. See ${docsUrl}`,
|
|
1014
|
+
docsUrl
|
|
1015
|
+
);
|
|
1016
|
+
this.name = "MissingProviderError";
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
var MissingProductRefError = class extends SolvaPayError {
|
|
1020
|
+
constructor(primitiveName) {
|
|
1021
|
+
const docsUrl = `${DOCS_BASE_URL}/primitives/product-ref`;
|
|
1022
|
+
super(
|
|
1023
|
+
"MISSING_PRODUCT_REF",
|
|
1024
|
+
`${primitiveName} requires a productRef. Pass the \`productRef\` prop (or set NEXT_PUBLIC_SOLVAPAY_PRODUCT_REF). See ${docsUrl}`,
|
|
1025
|
+
docsUrl
|
|
1026
|
+
);
|
|
1027
|
+
this.name = "MissingProductRefError";
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// src/primitives/CheckoutSummary.tsx
|
|
1032
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1033
|
+
var CheckoutSummaryContext = (0, import_react9.createContext)(null);
|
|
1034
|
+
function useCheckoutSummaryContext(part) {
|
|
1035
|
+
const ctx = (0, import_react9.useContext)(CheckoutSummaryContext);
|
|
1036
|
+
if (!ctx) {
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
`CheckoutSummary.${part} must be rendered inside <CheckoutSummary.Root>.`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return ctx;
|
|
1042
|
+
}
|
|
1043
|
+
var Root = (0, import_react9.forwardRef)(function CheckoutSummaryRoot({ planRef, productRef, asChild, children, ...rest }, forwardedRef) {
|
|
1044
|
+
const solva = (0, import_react9.useContext)(SolvaPayContext);
|
|
1045
|
+
if (!solva) throw new MissingProviderError("CheckoutSummary");
|
|
1046
|
+
const locale = useLocale();
|
|
1047
|
+
const copy = useCopy();
|
|
1048
|
+
const planSelection = usePlanSelection();
|
|
1049
|
+
const resolvedPlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
|
|
1050
|
+
const resolvedProductRef = productRef ?? planSelection?.productRef;
|
|
1051
|
+
const { plan, loading: planLoading } = usePlan({
|
|
1052
|
+
planRef: resolvedPlanRef,
|
|
1053
|
+
productRef: resolvedProductRef
|
|
1054
|
+
});
|
|
1055
|
+
const { product, loading: productLoading } = useProduct(resolvedProductRef);
|
|
1056
|
+
const loading = planLoading || productLoading;
|
|
1057
|
+
const priceFormatted = (0, import_react9.useMemo)(() => {
|
|
1058
|
+
const price = plan?.price ?? 0;
|
|
1059
|
+
const currency = plan?.currency ?? "usd";
|
|
1060
|
+
return formatPrice(price, currency, {
|
|
1061
|
+
locale,
|
|
1062
|
+
interval: plan?.interval,
|
|
1063
|
+
intervalCount: 1,
|
|
1064
|
+
free: copy.interval.free
|
|
1065
|
+
});
|
|
1066
|
+
}, [plan, locale, copy.interval.free]);
|
|
1067
|
+
const trialBanner = (0, import_react9.useMemo)(() => {
|
|
1068
|
+
const trialDays = plan?.trialDays;
|
|
1069
|
+
if (!trialDays || trialDays <= 0) return null;
|
|
1070
|
+
return interpolate(copy.interval.trial, { trialDays });
|
|
1071
|
+
}, [plan?.trialDays, copy.interval.trial]);
|
|
1072
|
+
const ctx = (0, import_react9.useMemo)(
|
|
1073
|
+
() => ({ plan, product, priceFormatted, trialBanner, loading }),
|
|
1074
|
+
[plan, product, priceFormatted, trialBanner, loading]
|
|
1075
|
+
);
|
|
1076
|
+
const Comp = asChild ? Slot : "div";
|
|
1077
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary": "", ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CheckoutSummaryContext.Provider, { value: ctx, children }) });
|
|
1078
|
+
});
|
|
1079
|
+
var ProductSlot = (0, import_react9.forwardRef)(function CheckoutSummaryProduct({ asChild, children, ...rest }, forwardedRef) {
|
|
1080
|
+
const ctx = useCheckoutSummaryContext("Product");
|
|
1081
|
+
const name = ctx.product?.name;
|
|
1082
|
+
if (!name) return null;
|
|
1083
|
+
const Comp = asChild ? Slot : "span";
|
|
1084
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-product": "", ...rest, children: children ?? name });
|
|
1085
|
+
});
|
|
1086
|
+
var PlanSlot = (0, import_react9.forwardRef)(function CheckoutSummaryPlan({ asChild, children, ...rest }, forwardedRef) {
|
|
1087
|
+
const ctx = useCheckoutSummaryContext("Plan");
|
|
1088
|
+
const name = ctx.plan?.name;
|
|
1089
|
+
if (!name) return null;
|
|
1090
|
+
const Comp = asChild ? Slot : "span";
|
|
1091
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-plan": "", ...rest, children: children ?? name });
|
|
1092
|
+
});
|
|
1093
|
+
var PriceSlot = (0, import_react9.forwardRef)(function CheckoutSummaryPrice({ asChild, children, ...rest }, forwardedRef) {
|
|
1094
|
+
const ctx = useCheckoutSummaryContext("Price");
|
|
1095
|
+
const Comp = asChild ? Slot : "span";
|
|
1096
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-price": "", ...rest, children: children ?? ctx.priceFormatted });
|
|
1097
|
+
});
|
|
1098
|
+
var TrialSlot = (0, import_react9.forwardRef)(function CheckoutSummaryTrial({ asChild, children, ...rest }, forwardedRef) {
|
|
1099
|
+
const ctx = useCheckoutSummaryContext("Trial");
|
|
1100
|
+
if (!ctx.trialBanner) return null;
|
|
1101
|
+
const Comp = asChild ? Slot : "span";
|
|
1102
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-trial": "", ...rest, children: children ?? ctx.trialBanner });
|
|
1103
|
+
});
|
|
1104
|
+
var TaxNoteSlot = (0, import_react9.forwardRef)(function CheckoutSummaryTaxNote({ asChild, children, ...rest }, forwardedRef) {
|
|
1105
|
+
useCheckoutSummaryContext("TaxNote");
|
|
1106
|
+
const Comp = asChild ? Slot : "span";
|
|
1107
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Comp, { ref: forwardedRef, "data-solvapay-checkout-summary-tax-note": "", ...rest, children: children ?? "Taxes calculated at checkout" });
|
|
1108
|
+
});
|
|
1109
|
+
var CheckoutSummaryRoot2 = Root;
|
|
1110
|
+
var CheckoutSummaryProduct2 = ProductSlot;
|
|
1111
|
+
var CheckoutSummaryPlan2 = PlanSlot;
|
|
1112
|
+
var CheckoutSummaryPrice2 = PriceSlot;
|
|
1113
|
+
var CheckoutSummaryTrial2 = TrialSlot;
|
|
1114
|
+
var CheckoutSummaryTaxNote2 = TaxNoteSlot;
|
|
1115
|
+
var CheckoutSummary = {
|
|
1116
|
+
Root,
|
|
1117
|
+
Product: ProductSlot,
|
|
1118
|
+
Plan: PlanSlot,
|
|
1119
|
+
Price: PriceSlot,
|
|
1120
|
+
Trial: TrialSlot,
|
|
1121
|
+
TaxNote: TaxNoteSlot
|
|
1122
|
+
};
|
|
1123
|
+
function useCheckoutSummary() {
|
|
1124
|
+
return useCheckoutSummaryContext("useCheckoutSummary");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/primitives/PlanSelector.tsx
|
|
1128
|
+
var import_react10 = require("react");
|
|
1129
|
+
|
|
1130
|
+
// src/hooks/usePurchase.ts
|
|
1131
|
+
function usePurchase() {
|
|
1132
|
+
const { purchase, refetchPurchase } = useSolvaPay();
|
|
1133
|
+
return {
|
|
1134
|
+
...purchase,
|
|
1135
|
+
refetch: refetchPurchase
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/primitives/PlanSelector.tsx
|
|
1140
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1141
|
+
var PlanSelectorContext = (0, import_react10.createContext)(null);
|
|
1142
|
+
function usePlanSelectorContext(part) {
|
|
1143
|
+
const ctx = (0, import_react10.useContext)(PlanSelectorContext);
|
|
1144
|
+
if (!ctx) {
|
|
1145
|
+
throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Root>.`);
|
|
1146
|
+
}
|
|
1147
|
+
return ctx;
|
|
1148
|
+
}
|
|
1149
|
+
var CardContext = (0, import_react10.createContext)(null);
|
|
1150
|
+
function useCardContext(part) {
|
|
1151
|
+
const ctx = (0, import_react10.useContext)(CardContext);
|
|
1152
|
+
if (!ctx) {
|
|
1153
|
+
throw new Error(`PlanSelector.${part} must be rendered inside <PlanSelector.Card>.`);
|
|
1154
|
+
}
|
|
1155
|
+
return ctx;
|
|
1156
|
+
}
|
|
1157
|
+
async function defaultListPlans(productRef, config) {
|
|
1158
|
+
const base = config?.api?.listPlans || "/api/list-plans";
|
|
1159
|
+
const url = `${base}?productRef=${encodeURIComponent(productRef)}`;
|
|
1160
|
+
const fetchFn = config?.fetch || fetch;
|
|
1161
|
+
const { headers } = await buildRequestHeaders(config);
|
|
1162
|
+
const res = await fetchFn(url, { method: "GET", headers });
|
|
1163
|
+
if (!res.ok) {
|
|
1164
|
+
const error = new Error(`Failed to fetch plans: ${res.statusText || res.status}`);
|
|
1165
|
+
config?.onError?.(error, "listPlans");
|
|
1166
|
+
throw error;
|
|
1167
|
+
}
|
|
1168
|
+
const data = await res.json();
|
|
1169
|
+
return data.plans ?? [];
|
|
1170
|
+
}
|
|
1171
|
+
var Root2 = (0, import_react10.forwardRef)(function PlanSelectorRoot(props, forwardedRef) {
|
|
1172
|
+
const {
|
|
1173
|
+
productRef,
|
|
1174
|
+
fetcher,
|
|
1175
|
+
filter,
|
|
1176
|
+
sortBy,
|
|
1177
|
+
autoSelectFirstPaid = true,
|
|
1178
|
+
initialPlanRef,
|
|
1179
|
+
currentPlanRef,
|
|
1180
|
+
popularPlanRef,
|
|
1181
|
+
onSelect,
|
|
1182
|
+
children,
|
|
1183
|
+
...rest
|
|
1184
|
+
} = props;
|
|
1185
|
+
const solva = (0, import_react10.useContext)(SolvaPayContext);
|
|
1186
|
+
if (!solva) throw new MissingProviderError("PlanSelector");
|
|
1187
|
+
if (!productRef) throw new MissingProductRefError("PlanSelector");
|
|
1188
|
+
const { _config } = solva;
|
|
1189
|
+
const { purchases } = usePurchase();
|
|
1190
|
+
const effectiveFetcher = (0, import_react10.useMemo)(
|
|
1191
|
+
() => fetcher ?? ((ref) => defaultListPlans(ref, _config)),
|
|
1192
|
+
[fetcher, _config]
|
|
1193
|
+
);
|
|
1194
|
+
const { plans, selectedPlan, selectPlan, loading, error } = usePlans({
|
|
1195
|
+
productRef,
|
|
1196
|
+
fetcher: effectiveFetcher,
|
|
1197
|
+
filter,
|
|
1198
|
+
sortBy,
|
|
1199
|
+
autoSelectFirstPaid,
|
|
1200
|
+
initialPlanRef
|
|
1201
|
+
});
|
|
1202
|
+
const autoCurrentPlanRef = (0, import_react10.useMemo)(() => {
|
|
1203
|
+
const active = purchases.filter((p) => p.status === "active").sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime())[0];
|
|
1204
|
+
return active?.planSnapshot?.reference ?? null;
|
|
1205
|
+
}, [purchases]);
|
|
1206
|
+
const resolvedCurrentPlanRef = currentPlanRef === null ? null : currentPlanRef ?? autoCurrentPlanRef;
|
|
1207
|
+
const isCurrent = (0, import_react10.useCallback)(
|
|
1208
|
+
(ref) => resolvedCurrentPlanRef === ref,
|
|
1209
|
+
[resolvedCurrentPlanRef]
|
|
1210
|
+
);
|
|
1211
|
+
const isFree = (0, import_react10.useCallback)(
|
|
1212
|
+
(ref) => plans.find((p) => p.reference === ref)?.requiresPayment === false,
|
|
1213
|
+
[plans]
|
|
1214
|
+
);
|
|
1215
|
+
const isPopular = (0, import_react10.useCallback)(
|
|
1216
|
+
(ref) => popularPlanRef === ref,
|
|
1217
|
+
[popularPlanRef]
|
|
1218
|
+
);
|
|
1219
|
+
const select = (0, import_react10.useCallback)(
|
|
1220
|
+
(ref) => {
|
|
1221
|
+
const plan = plans.find((p) => p.reference === ref);
|
|
1222
|
+
if (!plan) return;
|
|
1223
|
+
if (plan.requiresPayment === false) return;
|
|
1224
|
+
selectPlan(ref);
|
|
1225
|
+
onSelect?.(ref, plan);
|
|
1226
|
+
},
|
|
1227
|
+
[plans, selectPlan, onSelect]
|
|
1228
|
+
);
|
|
1229
|
+
const selectedPlanRef = selectedPlan?.reference ?? null;
|
|
1230
|
+
const ctx = (0, import_react10.useMemo)(
|
|
1231
|
+
() => ({
|
|
1232
|
+
plans,
|
|
1233
|
+
loading,
|
|
1234
|
+
error,
|
|
1235
|
+
selectedPlan: selectedPlan ?? null,
|
|
1236
|
+
selectedPlanRef,
|
|
1237
|
+
popularPlanRef,
|
|
1238
|
+
currentPlanRef: resolvedCurrentPlanRef,
|
|
1239
|
+
isCurrent,
|
|
1240
|
+
isFree,
|
|
1241
|
+
isPopular,
|
|
1242
|
+
select
|
|
1243
|
+
}),
|
|
1244
|
+
[
|
|
1245
|
+
plans,
|
|
1246
|
+
loading,
|
|
1247
|
+
error,
|
|
1248
|
+
selectedPlan,
|
|
1249
|
+
selectedPlanRef,
|
|
1250
|
+
popularPlanRef,
|
|
1251
|
+
resolvedCurrentPlanRef,
|
|
1252
|
+
isCurrent,
|
|
1253
|
+
isFree,
|
|
1254
|
+
isPopular,
|
|
1255
|
+
select
|
|
1256
|
+
]
|
|
1257
|
+
);
|
|
1258
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1259
|
+
PlanSelectionProvider,
|
|
1260
|
+
{
|
|
1261
|
+
value: {
|
|
1262
|
+
productRef,
|
|
1263
|
+
selectedPlanRef,
|
|
1264
|
+
setSelectedPlanRef: (ref) => {
|
|
1265
|
+
if (ref) select(ref);
|
|
1266
|
+
},
|
|
1267
|
+
plans,
|
|
1268
|
+
loading,
|
|
1269
|
+
error
|
|
1270
|
+
},
|
|
1271
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: forwardedRef, "data-solvapay-plan-selector": "", ...rest, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PlanSelectorContext.Provider, { value: ctx, children }) })
|
|
1272
|
+
}
|
|
1273
|
+
);
|
|
1274
|
+
});
|
|
1275
|
+
var Heading = (0, import_react10.forwardRef)(function PlanSelectorHeading({ asChild, children, ...rest }, forwardedRef) {
|
|
1276
|
+
usePlanSelectorContext("Heading");
|
|
1277
|
+
const copy = useCopy();
|
|
1278
|
+
const Comp = asChild ? Slot : "h3";
|
|
1279
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-heading": "", ...rest, children: children ?? copy.planSelector.heading });
|
|
1280
|
+
});
|
|
1281
|
+
var Grid = (0, import_react10.forwardRef)(function PlanSelectorGrid({ children, ...rest }, forwardedRef) {
|
|
1282
|
+
const ctx = usePlanSelectorContext("Grid");
|
|
1283
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { ref: forwardedRef, "data-solvapay-plan-selector-grid": "", ...rest, children: ctx.plans.map((plan) => {
|
|
1284
|
+
const isCurrent = ctx.isCurrent(plan.reference);
|
|
1285
|
+
const isFree = ctx.isFree(plan.reference);
|
|
1286
|
+
const isPopular = ctx.isPopular(plan.reference);
|
|
1287
|
+
const selected = ctx.selectedPlanRef === plan.reference;
|
|
1288
|
+
const disabled = isCurrent || isFree;
|
|
1289
|
+
const state = isCurrent ? "current" : isFree ? "disabled" : selected ? "selected" : "idle";
|
|
1290
|
+
const cardCtx = {
|
|
1291
|
+
plan,
|
|
1292
|
+
state,
|
|
1293
|
+
isCurrent,
|
|
1294
|
+
isFree,
|
|
1295
|
+
isPopular,
|
|
1296
|
+
disabled,
|
|
1297
|
+
select: () => ctx.select(plan.reference)
|
|
1298
|
+
};
|
|
1299
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CardContext.Provider, { value: cardCtx, children }, plan.reference);
|
|
1300
|
+
}) });
|
|
1301
|
+
});
|
|
1302
|
+
var Card = (0, import_react10.forwardRef)(function PlanSelectorCard({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
1303
|
+
const card = useCardContext("Card");
|
|
1304
|
+
const dataTrial = !!(card.plan.trialDays && card.plan.trialDays > 0);
|
|
1305
|
+
const commonProps = {
|
|
1306
|
+
"data-solvapay-plan-selector-card": "",
|
|
1307
|
+
"data-state": card.state,
|
|
1308
|
+
"data-free": card.isFree ? "" : void 0,
|
|
1309
|
+
"data-popular": card.isPopular ? "" : void 0,
|
|
1310
|
+
"data-trial": dataTrial ? "" : void 0,
|
|
1311
|
+
"aria-disabled": card.disabled || void 0,
|
|
1312
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
1313
|
+
if (!card.disabled) card.select();
|
|
1314
|
+
}),
|
|
1315
|
+
...rest
|
|
1316
|
+
};
|
|
1317
|
+
if (asChild) {
|
|
1318
|
+
return (
|
|
1319
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1320
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Slot, { ref: forwardedRef, ...commonProps, children })
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1324
|
+
"button",
|
|
1325
|
+
{
|
|
1326
|
+
ref: forwardedRef,
|
|
1327
|
+
type: "button",
|
|
1328
|
+
disabled: card.disabled,
|
|
1329
|
+
...commonProps,
|
|
1330
|
+
children
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
});
|
|
1334
|
+
var CardName = (0, import_react10.forwardRef)(function PlanSelectorCardName({ asChild, children, ...rest }, forwardedRef) {
|
|
1335
|
+
const card = useCardContext("CardName");
|
|
1336
|
+
if (!card.plan.name) return null;
|
|
1337
|
+
const Comp = asChild ? Slot : "span";
|
|
1338
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-name": "", ...rest, children: children ?? card.plan.name });
|
|
1339
|
+
});
|
|
1340
|
+
var CardPrice = (0, import_react10.forwardRef)(function PlanSelectorCardPrice({ asChild, children, ...rest }, forwardedRef) {
|
|
1341
|
+
const card = useCardContext("CardPrice");
|
|
1342
|
+
const locale = useLocale();
|
|
1343
|
+
const copy = useCopy();
|
|
1344
|
+
const formatted = (0, import_react10.useMemo)(() => {
|
|
1345
|
+
if (card.isFree) return copy.planSelector.freeBadge;
|
|
1346
|
+
return formatPrice(card.plan.price ?? 0, card.plan.currency ?? "usd", {
|
|
1347
|
+
locale,
|
|
1348
|
+
free: copy.interval.free
|
|
1349
|
+
});
|
|
1350
|
+
}, [
|
|
1351
|
+
card.isFree,
|
|
1352
|
+
card.plan.price,
|
|
1353
|
+
card.plan.currency,
|
|
1354
|
+
locale,
|
|
1355
|
+
copy.planSelector.freeBadge,
|
|
1356
|
+
copy.interval.free
|
|
1357
|
+
]);
|
|
1358
|
+
const Comp = asChild ? Slot : "span";
|
|
1359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-price": "", ...rest, children: children ?? formatted });
|
|
1360
|
+
});
|
|
1361
|
+
var CardInterval = (0, import_react10.forwardRef)(function PlanSelectorCardInterval({ asChild, children, ...rest }, forwardedRef) {
|
|
1362
|
+
const card = useCardContext("CardInterval");
|
|
1363
|
+
const copy = useCopy();
|
|
1364
|
+
if (card.isFree || !card.plan.interval) return null;
|
|
1365
|
+
const text = interpolate(copy.planSelector.perIntervalShort, { interval: card.plan.interval });
|
|
1366
|
+
const Comp = asChild ? Slot : "span";
|
|
1367
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-card-interval": "", ...rest, children: children ?? text });
|
|
1368
|
+
});
|
|
1369
|
+
var CardBadge = (0, import_react10.forwardRef)(function PlanSelectorCardBadge({ asChild, children, ...rest }, forwardedRef) {
|
|
1370
|
+
const card = useCardContext("CardBadge");
|
|
1371
|
+
const copy = useCopy();
|
|
1372
|
+
let variant = null;
|
|
1373
|
+
let label = "";
|
|
1374
|
+
if (card.isCurrent) {
|
|
1375
|
+
variant = "current";
|
|
1376
|
+
label = copy.planSelector.currentBadge;
|
|
1377
|
+
} else if (card.isPopular && !card.isFree) {
|
|
1378
|
+
variant = "popular";
|
|
1379
|
+
label = copy.planSelector.popularBadge;
|
|
1380
|
+
}
|
|
1381
|
+
if (!variant) return null;
|
|
1382
|
+
const Comp = asChild ? Slot : "span";
|
|
1383
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1384
|
+
Comp,
|
|
1385
|
+
{
|
|
1386
|
+
ref: forwardedRef,
|
|
1387
|
+
"data-solvapay-plan-selector-card-badge": "",
|
|
1388
|
+
"data-variant": variant,
|
|
1389
|
+
...rest,
|
|
1390
|
+
children: children ?? label
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
});
|
|
1394
|
+
var Loading = (0, import_react10.forwardRef)(function PlanSelectorLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
1395
|
+
const ctx = usePlanSelectorContext("Loading");
|
|
1396
|
+
if (!ctx.loading || ctx.plans.length > 0) return null;
|
|
1397
|
+
const Comp = asChild ? Slot : "div";
|
|
1398
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-loading": "", ...rest, children });
|
|
1399
|
+
});
|
|
1400
|
+
var ErrorSlot = (0, import_react10.forwardRef)(function PlanSelectorError({ asChild, children, ...rest }, forwardedRef) {
|
|
1401
|
+
const ctx = usePlanSelectorContext("Error");
|
|
1402
|
+
if (!ctx.error) return null;
|
|
1403
|
+
const Comp = asChild ? Slot : "div";
|
|
1404
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Comp, { ref: forwardedRef, "data-solvapay-plan-selector-error": "", role: "alert", ...rest, children: children ?? ctx.error.message });
|
|
1405
|
+
});
|
|
1406
|
+
var PlanSelectorRoot2 = Root2;
|
|
1407
|
+
var PlanSelectorHeading2 = Heading;
|
|
1408
|
+
var PlanSelectorGrid2 = Grid;
|
|
1409
|
+
var PlanSelectorCard2 = Card;
|
|
1410
|
+
var PlanSelectorCardName2 = CardName;
|
|
1411
|
+
var PlanSelectorCardPrice2 = CardPrice;
|
|
1412
|
+
var PlanSelectorCardInterval2 = CardInterval;
|
|
1413
|
+
var PlanSelectorCardBadge2 = CardBadge;
|
|
1414
|
+
var PlanSelectorLoading2 = Loading;
|
|
1415
|
+
var PlanSelectorError2 = ErrorSlot;
|
|
1416
|
+
var PlanSelector = {
|
|
1417
|
+
Root: Root2,
|
|
1418
|
+
Heading,
|
|
1419
|
+
Grid,
|
|
1420
|
+
Card,
|
|
1421
|
+
CardName,
|
|
1422
|
+
CardPrice,
|
|
1423
|
+
CardInterval,
|
|
1424
|
+
CardBadge,
|
|
1425
|
+
Loading,
|
|
1426
|
+
Error: ErrorSlot
|
|
1427
|
+
};
|
|
1428
|
+
function usePlanSelector() {
|
|
1429
|
+
return usePlanSelectorContext("usePlanSelector");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/primitives/PaymentForm.tsx
|
|
1433
|
+
var import_react16 = require("react");
|
|
1434
|
+
var import_react_stripe_js = require("@stripe/react-stripe-js");
|
|
1435
|
+
|
|
1436
|
+
// src/hooks/useCheckout.ts
|
|
1437
|
+
var import_react11 = require("react");
|
|
1438
|
+
var import_stripe_js = require("@stripe/stripe-js");
|
|
1439
|
+
var stripePromiseCache = /* @__PURE__ */ new Map();
|
|
1440
|
+
function getStripeCacheKey(publishableKey, accountId) {
|
|
1441
|
+
return accountId ? `${publishableKey}:${accountId}` : publishableKey;
|
|
1442
|
+
}
|
|
1443
|
+
async function resolvePlanRef(productRef, fetchFn, headers, listPlansRoute) {
|
|
1444
|
+
const url = `${listPlansRoute}?productRef=${encodeURIComponent(productRef)}`;
|
|
1445
|
+
const res = await fetchFn(url, { method: "GET", headers });
|
|
1446
|
+
if (!res.ok) {
|
|
1447
|
+
throw new Error(`Failed to fetch plans for product "${productRef}": ${res.statusText}`);
|
|
1448
|
+
}
|
|
1449
|
+
const data = await res.json();
|
|
1450
|
+
const allPlans = data.plans ?? [];
|
|
1451
|
+
const activePlans = allPlans.filter((p) => p.isActive !== false && p.status !== "inactive");
|
|
1452
|
+
if (activePlans.length === 0) {
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
`No active plans found for product "${productRef}". Configure at least one plan in the SolvaPay Console.`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
if (activePlans.length === 1) {
|
|
1458
|
+
return activePlans[0].reference;
|
|
1459
|
+
}
|
|
1460
|
+
const defaultPlan = activePlans.find((p) => p.default === true);
|
|
1461
|
+
if (defaultPlan) {
|
|
1462
|
+
return defaultPlan.reference;
|
|
1463
|
+
}
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
`Product "${productRef}" has ${activePlans.length} active plans but none is marked as default. Either pass planRef explicitly, use <PricingSelector> for user selection, or mark one plan as default in the SolvaPay Console.`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
function useCheckout(options) {
|
|
1469
|
+
const { planRef, productRef, customer } = options;
|
|
1470
|
+
const { createPayment, customerRef, updateCustomerRef, _config } = useSolvaPay();
|
|
1471
|
+
const [loading, setLoading] = (0, import_react11.useState)(false);
|
|
1472
|
+
const [error, setError] = (0, import_react11.useState)(null);
|
|
1473
|
+
const [stripePromise, setStripePromise] = (0, import_react11.useState)(null);
|
|
1474
|
+
const [clientSecret, setClientSecret] = (0, import_react11.useState)(null);
|
|
1475
|
+
const [resolvedPlanRef, setResolvedPlanRef] = (0, import_react11.useState)(planRef || null);
|
|
1476
|
+
const isStartingRef = (0, import_react11.useRef)(false);
|
|
1477
|
+
const startCheckout = (0, import_react11.useCallback)(async () => {
|
|
1478
|
+
if (isStartingRef.current || loading) {
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (!planRef && !productRef) {
|
|
1482
|
+
setError(
|
|
1483
|
+
new Error(
|
|
1484
|
+
"useCheckout: either planRef or productRef is required. Pass planRef directly, or pass productRef to auto-resolve the plan."
|
|
1485
|
+
)
|
|
1486
|
+
);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
isStartingRef.current = true;
|
|
1490
|
+
setLoading(true);
|
|
1491
|
+
setError(null);
|
|
1492
|
+
try {
|
|
1493
|
+
let effectivePlanRef = planRef;
|
|
1494
|
+
if (!effectivePlanRef && productRef) {
|
|
1495
|
+
const listPlansRoute = _config?.api?.listPlans || "/api/list-plans";
|
|
1496
|
+
effectivePlanRef = await resolvePlanRef(productRef, fetch, {}, listPlansRoute);
|
|
1497
|
+
setResolvedPlanRef(effectivePlanRef);
|
|
1498
|
+
}
|
|
1499
|
+
if (!effectivePlanRef) {
|
|
1500
|
+
throw new Error("Could not determine plan reference for checkout");
|
|
1501
|
+
}
|
|
1502
|
+
const result = await createPayment({
|
|
1503
|
+
planRef: effectivePlanRef,
|
|
1504
|
+
productRef,
|
|
1505
|
+
customer
|
|
1506
|
+
});
|
|
1507
|
+
if (!result || typeof result !== "object") {
|
|
1508
|
+
throw new Error("Invalid payment intent response from server");
|
|
1509
|
+
}
|
|
1510
|
+
if (!result.clientSecret || typeof result.clientSecret !== "string") {
|
|
1511
|
+
throw new Error("Invalid client secret in payment intent response");
|
|
1512
|
+
}
|
|
1513
|
+
if (!result.publishableKey || typeof result.publishableKey !== "string") {
|
|
1514
|
+
throw new Error("Invalid publishable key in payment intent response");
|
|
1515
|
+
}
|
|
1516
|
+
if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
|
|
1517
|
+
updateCustomerRef(result.customerRef);
|
|
1518
|
+
}
|
|
1519
|
+
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
1520
|
+
const cacheKey = getStripeCacheKey(result.publishableKey, result.accountId);
|
|
1521
|
+
let stripe = stripePromiseCache.get(cacheKey);
|
|
1522
|
+
if (!stripe) {
|
|
1523
|
+
stripe = (0, import_stripe_js.loadStripe)(result.publishableKey, stripeOptions);
|
|
1524
|
+
stripePromiseCache.set(cacheKey, stripe);
|
|
1525
|
+
}
|
|
1526
|
+
setStripePromise(stripe);
|
|
1527
|
+
setClientSecret(result.clientSecret);
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start checkout");
|
|
1530
|
+
setError(error2);
|
|
1531
|
+
} finally {
|
|
1532
|
+
setLoading(false);
|
|
1533
|
+
isStartingRef.current = false;
|
|
1534
|
+
}
|
|
1535
|
+
}, [planRef, productRef, customer, createPayment, updateCustomerRef, loading, _config]);
|
|
1536
|
+
const reset = (0, import_react11.useCallback)(() => {
|
|
1537
|
+
isStartingRef.current = false;
|
|
1538
|
+
setLoading(false);
|
|
1539
|
+
setError(null);
|
|
1540
|
+
setStripePromise(null);
|
|
1541
|
+
setClientSecret(null);
|
|
1542
|
+
setResolvedPlanRef(planRef || null);
|
|
1543
|
+
}, [planRef]);
|
|
1544
|
+
return {
|
|
1545
|
+
loading,
|
|
1546
|
+
error,
|
|
1547
|
+
stripePromise,
|
|
1548
|
+
clientSecret,
|
|
1549
|
+
resolvedPlanRef,
|
|
1550
|
+
startCheckout,
|
|
1551
|
+
reset
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// src/hooks/useCustomer.ts
|
|
1556
|
+
function useCustomer() {
|
|
1557
|
+
const { purchase, customerRef } = useSolvaPay();
|
|
1558
|
+
return {
|
|
1559
|
+
customerRef: purchase.customerRef || customerRef,
|
|
1560
|
+
email: purchase.email,
|
|
1561
|
+
name: purchase.name,
|
|
1562
|
+
loading: purchase.loading
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/hooks/useActivation.ts
|
|
1567
|
+
var import_react12 = require("react");
|
|
1568
|
+
function useActivation() {
|
|
1569
|
+
const { activatePlan } = useSolvaPay();
|
|
1570
|
+
const copy = useCopy();
|
|
1571
|
+
const [state, setState] = (0, import_react12.useState)("idle");
|
|
1572
|
+
const [error, setError] = (0, import_react12.useState)(null);
|
|
1573
|
+
const [result, setResult] = (0, import_react12.useState)(null);
|
|
1574
|
+
const activate = (0, import_react12.useCallback)(
|
|
1575
|
+
async (params) => {
|
|
1576
|
+
setState("activating");
|
|
1577
|
+
setError(null);
|
|
1578
|
+
setResult(null);
|
|
1579
|
+
try {
|
|
1580
|
+
const data = await activatePlan(params);
|
|
1581
|
+
setResult(data);
|
|
1582
|
+
switch (data.status) {
|
|
1583
|
+
case "activated":
|
|
1584
|
+
case "already_active":
|
|
1585
|
+
setState("activated");
|
|
1586
|
+
break;
|
|
1587
|
+
case "topup_required":
|
|
1588
|
+
setState("topup_required");
|
|
1589
|
+
break;
|
|
1590
|
+
case "payment_required":
|
|
1591
|
+
setError(copy.activation.paymentRequired);
|
|
1592
|
+
setState("payment_required");
|
|
1593
|
+
break;
|
|
1594
|
+
case "invalid":
|
|
1595
|
+
setError(data.message || copy.activation.invalidConfiguration);
|
|
1596
|
+
setState("error");
|
|
1597
|
+
break;
|
|
1598
|
+
default:
|
|
1599
|
+
setError(copy.activation.unexpectedResponse);
|
|
1600
|
+
setState("error");
|
|
1601
|
+
}
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
setError(err instanceof Error ? err.message : copy.activation.failed);
|
|
1604
|
+
setState("error");
|
|
1605
|
+
}
|
|
1606
|
+
},
|
|
1607
|
+
[activatePlan, copy]
|
|
1608
|
+
);
|
|
1609
|
+
const reset = (0, import_react12.useCallback)(() => {
|
|
1610
|
+
setState("idle");
|
|
1611
|
+
setError(null);
|
|
1612
|
+
setResult(null);
|
|
1613
|
+
}, []);
|
|
1614
|
+
return { activate, state, error, result, reset };
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/components/PaymentFormContext.tsx
|
|
1618
|
+
var import_react13 = require("react");
|
|
1619
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1620
|
+
var PaymentFormContext = (0, import_react13.createContext)(null);
|
|
1621
|
+
function usePaymentForm() {
|
|
1622
|
+
const ctx = (0, import_react13.useContext)(PaymentFormContext);
|
|
1623
|
+
if (!ctx) {
|
|
1624
|
+
throw new Error(
|
|
1625
|
+
"PaymentForm subcomponents must be used inside a <PaymentForm>. Wrap the slots with <PaymentForm planRef=... productRef=...>."
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
return ctx;
|
|
1629
|
+
}
|
|
1630
|
+
var PaymentFormProvider = ({ value, children }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaymentFormContext.Provider, { value, children });
|
|
1631
|
+
|
|
1632
|
+
// src/components/CheckoutSummary.tsx
|
|
1633
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1634
|
+
var CheckoutSummary2 = ({
|
|
1635
|
+
planRef,
|
|
1636
|
+
productRef,
|
|
1637
|
+
showTrial = true,
|
|
1638
|
+
showTaxNote = false,
|
|
1639
|
+
className
|
|
1640
|
+
}) => {
|
|
1641
|
+
const rootClass = ["solvapay-checkout-summary", className].filter(Boolean).join(" ");
|
|
1642
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(CheckoutSummary.Root, { planRef, productRef, className: rootClass, children: [
|
|
1643
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutSummary.Product, { className: "solvapay-checkout-summary-product" }),
|
|
1644
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "solvapay-checkout-summary-row", children: [
|
|
1645
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutSummary.Plan, { className: "solvapay-checkout-summary-plan" }),
|
|
1646
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutSummary.Price, { className: "solvapay-checkout-summary-price" })
|
|
1647
|
+
] }),
|
|
1648
|
+
showTrial && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutSummary.Trial, { className: "solvapay-checkout-summary-trial" }),
|
|
1649
|
+
showTaxNote && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutSummary.TaxNote, { className: "solvapay-checkout-summary-tax-note" })
|
|
1650
|
+
] });
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
// src/primitives/MandateText.tsx
|
|
1654
|
+
var import_react15 = require("react");
|
|
1655
|
+
|
|
1656
|
+
// src/hooks/useMerchant.ts
|
|
1657
|
+
var import_react14 = require("react");
|
|
1658
|
+
var merchantCache = /* @__PURE__ */ new Map();
|
|
1659
|
+
var CACHE_DURATION4 = 5 * 60 * 1e3;
|
|
1660
|
+
function cacheKeyFor(config) {
|
|
1661
|
+
return config?.api?.getMerchant || "/api/merchant";
|
|
1662
|
+
}
|
|
1663
|
+
async function fetchMerchant(config) {
|
|
1664
|
+
const route = cacheKeyFor(config);
|
|
1665
|
+
const fetchFn = config?.fetch || fetch;
|
|
1666
|
+
const { headers } = await buildRequestHeaders(config);
|
|
1667
|
+
const res = await fetchFn(route, { method: "GET", headers });
|
|
1668
|
+
if (!res.ok) {
|
|
1669
|
+
const error = new Error(`Failed to fetch merchant: ${res.statusText || res.status}`);
|
|
1670
|
+
config?.onError?.(error, "getMerchant");
|
|
1671
|
+
throw error;
|
|
1672
|
+
}
|
|
1673
|
+
return await res.json();
|
|
1674
|
+
}
|
|
1675
|
+
function useMerchant() {
|
|
1676
|
+
const { _config } = useSolvaPay();
|
|
1677
|
+
const key = cacheKeyFor(_config);
|
|
1678
|
+
const [merchant, setMerchant] = (0, import_react14.useState)(
|
|
1679
|
+
() => merchantCache.get(key)?.merchant ?? null
|
|
1680
|
+
);
|
|
1681
|
+
const [loading, setLoading] = (0, import_react14.useState)(() => {
|
|
1682
|
+
const cached = merchantCache.get(key);
|
|
1683
|
+
return !cached || !cached.merchant && !cached.promise;
|
|
1684
|
+
});
|
|
1685
|
+
const [error, setError] = (0, import_react14.useState)(null);
|
|
1686
|
+
const load = (0, import_react14.useCallback)(
|
|
1687
|
+
async (force = false) => {
|
|
1688
|
+
const cached = merchantCache.get(key);
|
|
1689
|
+
const now = Date.now();
|
|
1690
|
+
if (!force && cached?.merchant && now - cached.timestamp < CACHE_DURATION4) {
|
|
1691
|
+
setMerchant(cached.merchant);
|
|
1692
|
+
setLoading(false);
|
|
1693
|
+
setError(null);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
if (!force && cached?.promise) {
|
|
1697
|
+
try {
|
|
1698
|
+
setLoading(true);
|
|
1699
|
+
const m = await cached.promise;
|
|
1700
|
+
setMerchant(m);
|
|
1701
|
+
setError(null);
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
setError(err instanceof Error ? err : new Error("Failed to load merchant"));
|
|
1704
|
+
} finally {
|
|
1705
|
+
setLoading(false);
|
|
1706
|
+
}
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
try {
|
|
1710
|
+
setLoading(true);
|
|
1711
|
+
setError(null);
|
|
1712
|
+
const promise = fetchMerchant(_config);
|
|
1713
|
+
merchantCache.set(key, { merchant: null, promise, timestamp: now });
|
|
1714
|
+
const m = await promise;
|
|
1715
|
+
merchantCache.set(key, { merchant: m, promise: null, timestamp: now });
|
|
1716
|
+
setMerchant(m);
|
|
1717
|
+
} catch (err) {
|
|
1718
|
+
merchantCache.delete(key);
|
|
1719
|
+
setError(err instanceof Error ? err : new Error("Failed to load merchant"));
|
|
1720
|
+
} finally {
|
|
1721
|
+
setLoading(false);
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
[_config, key]
|
|
1725
|
+
);
|
|
1726
|
+
(0, import_react14.useEffect)(() => {
|
|
1727
|
+
load();
|
|
1728
|
+
}, [load]);
|
|
1729
|
+
return {
|
|
1730
|
+
merchant,
|
|
1731
|
+
loading,
|
|
1732
|
+
error,
|
|
1733
|
+
refetch: () => load(true)
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
// src/utils/checkoutVariant.ts
|
|
1738
|
+
function deriveVariant(plan, mode) {
|
|
1739
|
+
if (mode === "topup") return "topup";
|
|
1740
|
+
if (!plan?.type) return "oneTime";
|
|
1741
|
+
if (plan.type === "usage-based") {
|
|
1742
|
+
return plan.billingModel === "post-paid" ? "usageMetered" : "topup";
|
|
1743
|
+
}
|
|
1744
|
+
if (plan.requiresPayment === false) return "freeTier";
|
|
1745
|
+
switch (plan.type) {
|
|
1746
|
+
case "recurring":
|
|
1747
|
+
return "recurring";
|
|
1748
|
+
case "one-time":
|
|
1749
|
+
default:
|
|
1750
|
+
return "oneTime";
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// src/primitives/MandateText.tsx
|
|
1755
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1756
|
+
var MandateText = (0, import_react15.forwardRef)(
|
|
1757
|
+
function MandateText2({
|
|
1758
|
+
planRef,
|
|
1759
|
+
productRef,
|
|
1760
|
+
variant,
|
|
1761
|
+
mode,
|
|
1762
|
+
amountMinor,
|
|
1763
|
+
currency,
|
|
1764
|
+
asChild,
|
|
1765
|
+
children,
|
|
1766
|
+
...rest
|
|
1767
|
+
}, forwardedRef) {
|
|
1768
|
+
const solva = (0, import_react15.useContext)(SolvaPayContext);
|
|
1769
|
+
if (!solva) throw new MissingProviderError("MandateText");
|
|
1770
|
+
const locale = useLocale();
|
|
1771
|
+
const copy = useCopy();
|
|
1772
|
+
const planSelection = usePlanSelection();
|
|
1773
|
+
const resolvedPlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
|
|
1774
|
+
const resolvedProductRef = productRef ?? planSelection?.productRef;
|
|
1775
|
+
const { plan } = usePlan({ planRef: resolvedPlanRef, productRef: resolvedProductRef });
|
|
1776
|
+
const { product } = useProduct(resolvedProductRef);
|
|
1777
|
+
const { merchant } = useMerchant();
|
|
1778
|
+
const resolvedVariant = variant || deriveVariant(plan, mode);
|
|
1779
|
+
const effectiveAmount = amountMinor ?? plan?.price ?? 0;
|
|
1780
|
+
const effectiveCurrency = currency ?? plan?.currency ?? merchant?.defaultCurrency ?? "usd";
|
|
1781
|
+
const amountFormatted = formatPrice(effectiveAmount, effectiveCurrency, {
|
|
1782
|
+
locale,
|
|
1783
|
+
free: copy.interval.free
|
|
1784
|
+
});
|
|
1785
|
+
const ctx = (0, import_react15.useMemo)(
|
|
1786
|
+
() => ({
|
|
1787
|
+
merchant: {
|
|
1788
|
+
legalName: merchant?.legalName ?? merchant?.displayName ?? "",
|
|
1789
|
+
displayName: merchant?.displayName,
|
|
1790
|
+
supportEmail: merchant?.supportEmail,
|
|
1791
|
+
termsUrl: merchant?.termsUrl,
|
|
1792
|
+
privacyUrl: merchant?.privacyUrl
|
|
1793
|
+
},
|
|
1794
|
+
plan: plan ? {
|
|
1795
|
+
name: plan.name,
|
|
1796
|
+
interval: plan.interval,
|
|
1797
|
+
intervalCount: 1,
|
|
1798
|
+
trialDays: plan.trialDays,
|
|
1799
|
+
measures: plan.measures,
|
|
1800
|
+
billingCycle: plan.billingCycle
|
|
1801
|
+
} : void 0,
|
|
1802
|
+
product: product ? { name: product.name } : void 0,
|
|
1803
|
+
amountFormatted,
|
|
1804
|
+
trialDays: plan?.trialDays
|
|
1805
|
+
}),
|
|
1806
|
+
[merchant, plan, product, amountFormatted]
|
|
1807
|
+
);
|
|
1808
|
+
const template = copy.mandate[resolvedVariant];
|
|
1809
|
+
const text = typeof template === "function" ? template(ctx) : template;
|
|
1810
|
+
if (!text) return null;
|
|
1811
|
+
const Comp = asChild ? Slot : "p";
|
|
1812
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1813
|
+
Comp,
|
|
1814
|
+
{
|
|
1815
|
+
ref: forwardedRef,
|
|
1816
|
+
"data-solvapay-mandate-text": "",
|
|
1817
|
+
"data-variant": resolvedVariant,
|
|
1818
|
+
...rest,
|
|
1819
|
+
children: children ?? text
|
|
1820
|
+
}
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
|
|
1825
|
+
// src/components/Spinner.tsx
|
|
1826
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1827
|
+
var Spinner = ({
|
|
1828
|
+
className = "",
|
|
1829
|
+
size = "md"
|
|
1830
|
+
}) => {
|
|
1831
|
+
const sizeClasses = {
|
|
1832
|
+
sm: "h-4 w-4",
|
|
1833
|
+
md: "h-5 w-5",
|
|
1834
|
+
lg: "h-6 w-6"
|
|
1835
|
+
};
|
|
1836
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
1837
|
+
"svg",
|
|
1838
|
+
{
|
|
1839
|
+
className: `animate-spin ${sizeClasses[size]} ${className}`,
|
|
1840
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1841
|
+
fill: "none",
|
|
1842
|
+
viewBox: "0 0 24 24",
|
|
1843
|
+
role: "status",
|
|
1844
|
+
"aria-busy": "true",
|
|
1845
|
+
children: [
|
|
1846
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }),
|
|
1847
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1848
|
+
"path",
|
|
1849
|
+
{
|
|
1850
|
+
className: "opacity-75",
|
|
1851
|
+
fill: "currentColor",
|
|
1852
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
1853
|
+
}
|
|
1854
|
+
)
|
|
1855
|
+
]
|
|
1856
|
+
}
|
|
1857
|
+
);
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/utils/confirmPayment.ts
|
|
1861
|
+
async function confirmPayment(input) {
|
|
1862
|
+
const { stripe, elements, clientSecret, mode, returnUrl, billingDetails, copy } = input;
|
|
1863
|
+
try {
|
|
1864
|
+
if (mode === "payment-element") {
|
|
1865
|
+
const paymentElement = elements.getElement("payment");
|
|
1866
|
+
if (!paymentElement) {
|
|
1867
|
+
return { status: "error", message: copy.errors.cardElementMissing };
|
|
1868
|
+
}
|
|
1869
|
+
const { error: error2, paymentIntent: paymentIntent2 } = await stripe.confirmPayment({
|
|
1870
|
+
elements,
|
|
1871
|
+
clientSecret,
|
|
1872
|
+
confirmParams: {
|
|
1873
|
+
return_url: returnUrl,
|
|
1874
|
+
payment_method_data: billingDetails ? { billing_details: billingDetails } : void 0
|
|
1875
|
+
},
|
|
1876
|
+
redirect: "if_required"
|
|
1877
|
+
});
|
|
1878
|
+
if (error2) {
|
|
1879
|
+
return { status: "error", message: error2.message || copy.errors.paymentUnexpected };
|
|
1880
|
+
}
|
|
1881
|
+
return mapIntent(paymentIntent2, copy);
|
|
1882
|
+
}
|
|
1883
|
+
const cardElement = elements.getElement("card");
|
|
1884
|
+
if (!cardElement) {
|
|
1885
|
+
return { status: "error", message: copy.errors.cardElementMissing };
|
|
1886
|
+
}
|
|
1887
|
+
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
|
|
1888
|
+
payment_method: {
|
|
1889
|
+
card: cardElement,
|
|
1890
|
+
billing_details: billingDetails
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
if (error) {
|
|
1894
|
+
return { status: "error", message: error.message || copy.errors.paymentUnexpected };
|
|
1895
|
+
}
|
|
1896
|
+
return mapIntent(paymentIntent, copy);
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
return {
|
|
1899
|
+
status: "error",
|
|
1900
|
+
message: err instanceof Error ? err.message : copy.errors.paymentUnexpected
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function mapIntent(paymentIntent, copy) {
|
|
1905
|
+
if (!paymentIntent) {
|
|
1906
|
+
return { status: "error", message: copy.errors.paymentUnexpected };
|
|
1907
|
+
}
|
|
1908
|
+
if (paymentIntent.status === "succeeded") {
|
|
1909
|
+
return { status: "succeeded", paymentIntent };
|
|
1910
|
+
}
|
|
1911
|
+
if (paymentIntent.status === "requires_action") {
|
|
1912
|
+
return { status: "requires_action", message: copy.errors.paymentRequires3ds };
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
status: "other",
|
|
1916
|
+
message: interpolate(copy.errors.paymentStatusPrefix, {
|
|
1917
|
+
status: paymentIntent.status || "processing"
|
|
1918
|
+
}),
|
|
1919
|
+
paymentIntent
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// src/utils/processPaymentResult.ts
|
|
1924
|
+
async function reconcilePayment(input) {
|
|
1925
|
+
const { paymentIntentId, productRef, planRef, processPayment, refetchPurchase, copy } = input;
|
|
1926
|
+
if (!processPayment || !productRef) {
|
|
1927
|
+
try {
|
|
1928
|
+
await refetchPurchase();
|
|
1929
|
+
return { status: "success" };
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
return {
|
|
1932
|
+
status: "error",
|
|
1933
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
try {
|
|
1938
|
+
const result = await processPayment({ paymentIntentId, productRef, planRef });
|
|
1939
|
+
const isTimeout = result?.status === "timeout";
|
|
1940
|
+
if (isTimeout) {
|
|
1941
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
1942
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 1e3));
|
|
1943
|
+
await refetchPurchase();
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
status: "timeout",
|
|
1947
|
+
error: new Error(copy.errors.paymentProcessingTimeout)
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
await refetchPurchase();
|
|
1951
|
+
return { status: "success" };
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
return {
|
|
1954
|
+
status: "error",
|
|
1955
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/utils/checkoutCta.ts
|
|
1961
|
+
function resolveCta(input) {
|
|
1962
|
+
if (input.override) return input.override;
|
|
1963
|
+
const { variant, plan, product, amountFormatted, copy } = input;
|
|
1964
|
+
if (variant === "recurring") {
|
|
1965
|
+
if (plan?.trialDays && plan.trialDays > 0) {
|
|
1966
|
+
return interpolate(copy.cta.trialStart, { trialDays: plan.trialDays });
|
|
1967
|
+
}
|
|
1968
|
+
return copy.cta.subscribe;
|
|
1969
|
+
}
|
|
1970
|
+
if (variant === "oneTime") {
|
|
1971
|
+
return interpolate(copy.cta.payAmount, { amount: amountFormatted });
|
|
1972
|
+
}
|
|
1973
|
+
if (variant === "topup") {
|
|
1974
|
+
return interpolate(copy.cta.addAmount, { amount: amountFormatted });
|
|
1975
|
+
}
|
|
1976
|
+
if (variant === "usageMetered" || variant === "freeTier") {
|
|
1977
|
+
const productName = product?.name ?? (typeof plan?.metadata?.productName === "string" ? plan.metadata.productName : plan?.name ?? "service");
|
|
1978
|
+
return interpolate(copy.cta.startUsing, { product: productName });
|
|
1979
|
+
}
|
|
1980
|
+
return copy.cta.payNow;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/primitives/PaymentForm.tsx
|
|
1984
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1985
|
+
function toSubmitVariant(variant) {
|
|
1986
|
+
switch (variant) {
|
|
1987
|
+
case "freeTier":
|
|
1988
|
+
return "free";
|
|
1989
|
+
case "topup":
|
|
1990
|
+
return "topup";
|
|
1991
|
+
case "usageMetered":
|
|
1992
|
+
return "activate";
|
|
1993
|
+
default:
|
|
1994
|
+
return "paid";
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
var Root3 = (0, import_react16.forwardRef)(function PaymentFormRoot(props, forwardedRef) {
|
|
1998
|
+
const {
|
|
1999
|
+
planRef,
|
|
2000
|
+
productRef,
|
|
2001
|
+
onSuccess,
|
|
2002
|
+
onResult,
|
|
2003
|
+
onFreePlan,
|
|
2004
|
+
onError,
|
|
2005
|
+
returnUrl,
|
|
2006
|
+
submitButtonText,
|
|
2007
|
+
className,
|
|
2008
|
+
buttonClassName,
|
|
2009
|
+
prefillCustomer,
|
|
2010
|
+
requireTermsAcceptance = false,
|
|
2011
|
+
children
|
|
2012
|
+
} = props;
|
|
2013
|
+
const solva = (0, import_react16.useContext)(SolvaPayContext);
|
|
2014
|
+
if (!solva) throw new MissingProviderError("PaymentForm");
|
|
2015
|
+
const copy = useCopy();
|
|
2016
|
+
const locale = useLocale();
|
|
2017
|
+
const planSelection = usePlanSelection();
|
|
2018
|
+
const effectivePlanRef = planRef ?? planSelection?.selectedPlanRef ?? void 0;
|
|
2019
|
+
const effectiveProductRef = productRef ?? planSelection?.productRef;
|
|
2020
|
+
const { plan: resolvedPlan } = usePlan({
|
|
2021
|
+
planRef: effectivePlanRef,
|
|
2022
|
+
productRef: effectiveProductRef
|
|
2023
|
+
});
|
|
2024
|
+
const isFreePlan = resolvedPlan?.requiresPayment === false;
|
|
2025
|
+
const {
|
|
2026
|
+
loading: checkoutLoading,
|
|
2027
|
+
error: checkoutError,
|
|
2028
|
+
clientSecret,
|
|
2029
|
+
startCheckout,
|
|
2030
|
+
stripePromise,
|
|
2031
|
+
resolvedPlanRef
|
|
2032
|
+
} = useCheckout({
|
|
2033
|
+
planRef: effectivePlanRef,
|
|
2034
|
+
productRef: effectiveProductRef,
|
|
2035
|
+
customer: prefillCustomer
|
|
2036
|
+
});
|
|
2037
|
+
const hasInitializedRef = (0, import_react16.useRef)(false);
|
|
2038
|
+
const hasPlanOrProduct = !!(effectivePlanRef || effectiveProductRef);
|
|
2039
|
+
(0, import_react16.useEffect)(() => {
|
|
2040
|
+
if (isFreePlan) return;
|
|
2041
|
+
if (!hasInitializedRef.current && hasPlanOrProduct && !checkoutLoading && !checkoutError && !clientSecret) {
|
|
2042
|
+
hasInitializedRef.current = true;
|
|
2043
|
+
startCheckout().catch(() => {
|
|
2044
|
+
hasInitializedRef.current = false;
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
if (hasPlanOrProduct && clientSecret) {
|
|
2048
|
+
hasInitializedRef.current = true;
|
|
2049
|
+
}
|
|
2050
|
+
}, [
|
|
2051
|
+
hasPlanOrProduct,
|
|
2052
|
+
checkoutLoading,
|
|
2053
|
+
checkoutError,
|
|
2054
|
+
clientSecret,
|
|
2055
|
+
startCheckout,
|
|
2056
|
+
isFreePlan
|
|
2057
|
+
]);
|
|
2058
|
+
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
2059
|
+
const elementsOptions = (0, import_react16.useMemo)(() => {
|
|
2060
|
+
if (!clientSecret) return void 0;
|
|
2061
|
+
return { clientSecret, locale };
|
|
2062
|
+
}, [clientSecret, locale]);
|
|
2063
|
+
const shouldRenderElements = !!(stripePromise && clientSecret);
|
|
2064
|
+
if (!hasPlanOrProduct) {
|
|
2065
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2066
|
+
"div",
|
|
2067
|
+
{
|
|
2068
|
+
ref: forwardedRef,
|
|
2069
|
+
className,
|
|
2070
|
+
"data-solvapay-payment-form": "",
|
|
2071
|
+
"data-state": "error",
|
|
2072
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-error": "", children: copy.errors.configMissingPlanOrProduct })
|
|
2073
|
+
}
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
if (checkoutError) {
|
|
2077
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2078
|
+
"div",
|
|
2079
|
+
{
|
|
2080
|
+
ref: forwardedRef,
|
|
2081
|
+
className,
|
|
2082
|
+
"data-solvapay-payment-form": "",
|
|
2083
|
+
"data-state": "error",
|
|
2084
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-solvapay-payment-form-error": "", children: [
|
|
2085
|
+
copy.errors.paymentInitFailed,
|
|
2086
|
+
" ",
|
|
2087
|
+
checkoutError.message || copy.errors.unknownError
|
|
2088
|
+
] })
|
|
2089
|
+
}
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
if (isFreePlan && resolvedPlan) {
|
|
2093
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2094
|
+
"div",
|
|
2095
|
+
{
|
|
2096
|
+
ref: forwardedRef,
|
|
2097
|
+
className,
|
|
2098
|
+
"data-solvapay-payment-form": "",
|
|
2099
|
+
"data-state": "ready",
|
|
2100
|
+
"data-variant": "free",
|
|
2101
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2102
|
+
FreeInner,
|
|
2103
|
+
{
|
|
2104
|
+
planRef: effectivePlanRef,
|
|
2105
|
+
productRef: effectiveProductRef,
|
|
2106
|
+
plan: resolvedPlan,
|
|
2107
|
+
resolvedPlanRef,
|
|
2108
|
+
requireTermsAcceptance,
|
|
2109
|
+
submitButtonText,
|
|
2110
|
+
buttonClassName,
|
|
2111
|
+
onFreePlan,
|
|
2112
|
+
onResult,
|
|
2113
|
+
onError,
|
|
2114
|
+
children
|
|
2115
|
+
}
|
|
2116
|
+
)
|
|
2117
|
+
}
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
if (shouldRenderElements && elementsOptions) {
|
|
2121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2122
|
+
"div",
|
|
2123
|
+
{
|
|
2124
|
+
ref: forwardedRef,
|
|
2125
|
+
className,
|
|
2126
|
+
"data-solvapay-payment-form": "",
|
|
2127
|
+
"data-state": "ready",
|
|
2128
|
+
"data-variant": "paid",
|
|
2129
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_stripe_js.Elements, { stripe: stripePromise, options: elementsOptions, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2130
|
+
PaidInner,
|
|
2131
|
+
{
|
|
2132
|
+
planRef: effectivePlanRef,
|
|
2133
|
+
productRef: effectiveProductRef,
|
|
2134
|
+
prefillCustomer,
|
|
2135
|
+
resolvedPlanRef,
|
|
2136
|
+
plan: resolvedPlan ?? null,
|
|
2137
|
+
clientSecret,
|
|
2138
|
+
returnUrl: finalReturnUrl,
|
|
2139
|
+
submitButtonText,
|
|
2140
|
+
buttonClassName,
|
|
2141
|
+
requireTermsAcceptance,
|
|
2142
|
+
onSuccess,
|
|
2143
|
+
onResult,
|
|
2144
|
+
onError,
|
|
2145
|
+
children
|
|
2146
|
+
}
|
|
2147
|
+
) }, clientSecret)
|
|
2148
|
+
}
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2152
|
+
"div",
|
|
2153
|
+
{
|
|
2154
|
+
ref: forwardedRef,
|
|
2155
|
+
className,
|
|
2156
|
+
"data-solvapay-payment-form": "",
|
|
2157
|
+
"data-state": "loading",
|
|
2158
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, { size: "md" }) })
|
|
2159
|
+
}
|
|
2160
|
+
);
|
|
2161
|
+
});
|
|
2162
|
+
var PaidInner = ({
|
|
2163
|
+
planRef,
|
|
2164
|
+
productRef,
|
|
2165
|
+
prefillCustomer,
|
|
2166
|
+
resolvedPlanRef,
|
|
2167
|
+
plan,
|
|
2168
|
+
clientSecret,
|
|
2169
|
+
returnUrl,
|
|
2170
|
+
submitButtonText,
|
|
2171
|
+
buttonClassName,
|
|
2172
|
+
requireTermsAcceptance,
|
|
2173
|
+
onSuccess,
|
|
2174
|
+
onResult,
|
|
2175
|
+
onError,
|
|
2176
|
+
children
|
|
2177
|
+
}) => {
|
|
2178
|
+
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
2179
|
+
const elements = (0, import_react_stripe_js.useElements)();
|
|
2180
|
+
const copy = useCopy();
|
|
2181
|
+
const customer = useCustomer();
|
|
2182
|
+
const { processPayment } = useSolvaPay();
|
|
2183
|
+
const { refetch } = usePurchase();
|
|
2184
|
+
const [elementKind, setElementKind] = (0, import_react16.useState)(
|
|
2185
|
+
children ? null : "payment-element"
|
|
2186
|
+
);
|
|
2187
|
+
const [paymentInputComplete, setPaymentInputComplete] = (0, import_react16.useState)(false);
|
|
2188
|
+
const [termsAccepted, setTermsAccepted] = (0, import_react16.useState)(false);
|
|
2189
|
+
const [isProcessing, setIsProcessing] = (0, import_react16.useState)(false);
|
|
2190
|
+
const [error, setError] = (0, import_react16.useState)(null);
|
|
2191
|
+
const isReady = !!(stripe && elements);
|
|
2192
|
+
const canSubmit = isReady && !!clientSecret && !!elementKind && paymentInputComplete && (!requireTermsAcceptance || termsAccepted) && !isProcessing;
|
|
2193
|
+
const submit = (0, import_react16.useCallback)(async () => {
|
|
2194
|
+
if (!stripe || !elements || !clientSecret || !elementKind) {
|
|
2195
|
+
const msg2 = !stripe || !elements ? copy.errors.stripeUnavailable : copy.errors.paymentIntentUnavailable;
|
|
2196
|
+
setError(msg2);
|
|
2197
|
+
onError?.(new Error(msg2));
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
setError(null);
|
|
2201
|
+
setIsProcessing(true);
|
|
2202
|
+
const result = await confirmPayment({
|
|
2203
|
+
stripe,
|
|
2204
|
+
elements,
|
|
2205
|
+
clientSecret,
|
|
2206
|
+
mode: elementKind,
|
|
2207
|
+
returnUrl,
|
|
2208
|
+
billingDetails: {
|
|
2209
|
+
name: customer.name ?? prefillCustomer?.name,
|
|
2210
|
+
email: customer.email ?? prefillCustomer?.email
|
|
2211
|
+
},
|
|
2212
|
+
copy
|
|
2213
|
+
});
|
|
2214
|
+
if (result.status === "error") {
|
|
2215
|
+
setError(result.message);
|
|
2216
|
+
setIsProcessing(false);
|
|
2217
|
+
onError?.(new Error(result.message));
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (result.status === "requires_action" || result.status === "other") {
|
|
2221
|
+
setError(result.message);
|
|
2222
|
+
setIsProcessing(false);
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const paymentIntent = result.paymentIntent;
|
|
2226
|
+
const reconcileResult = await reconcilePayment({
|
|
2227
|
+
paymentIntentId: paymentIntent.id,
|
|
2228
|
+
productRef,
|
|
2229
|
+
planRef: planRef || resolvedPlanRef || void 0,
|
|
2230
|
+
processPayment,
|
|
2231
|
+
refetchPurchase: refetch,
|
|
2232
|
+
copy
|
|
2233
|
+
});
|
|
2234
|
+
setIsProcessing(false);
|
|
2235
|
+
if (reconcileResult.status === "success") {
|
|
2236
|
+
const pi = paymentIntent;
|
|
2237
|
+
onSuccess?.(pi);
|
|
2238
|
+
const paid = { kind: "paid", paymentIntent: pi };
|
|
2239
|
+
onResult?.(paid);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
const msg = reconcileResult.status === "timeout" ? reconcileResult.error.message : copy.errors.paymentProcessingFailed;
|
|
2243
|
+
setError(msg);
|
|
2244
|
+
onError?.(reconcileResult.error);
|
|
2245
|
+
}, [
|
|
2246
|
+
stripe,
|
|
2247
|
+
elements,
|
|
2248
|
+
clientSecret,
|
|
2249
|
+
elementKind,
|
|
2250
|
+
returnUrl,
|
|
2251
|
+
customer,
|
|
2252
|
+
prefillCustomer,
|
|
2253
|
+
copy,
|
|
2254
|
+
processPayment,
|
|
2255
|
+
productRef,
|
|
2256
|
+
planRef,
|
|
2257
|
+
resolvedPlanRef,
|
|
2258
|
+
refetch,
|
|
2259
|
+
onSuccess,
|
|
2260
|
+
onResult,
|
|
2261
|
+
onError
|
|
2262
|
+
]);
|
|
2263
|
+
const contextValue = (0, import_react16.useMemo)(
|
|
2264
|
+
() => ({
|
|
2265
|
+
planRef,
|
|
2266
|
+
productRef,
|
|
2267
|
+
prefillCustomer,
|
|
2268
|
+
resolvedPlanRef,
|
|
2269
|
+
plan,
|
|
2270
|
+
clientSecret,
|
|
2271
|
+
stripe: stripe ?? null,
|
|
2272
|
+
elements: elements ?? null,
|
|
2273
|
+
isProcessing,
|
|
2274
|
+
isReady,
|
|
2275
|
+
paymentInputComplete,
|
|
2276
|
+
termsAccepted,
|
|
2277
|
+
requireTermsAcceptance,
|
|
2278
|
+
canSubmit,
|
|
2279
|
+
error,
|
|
2280
|
+
elementKind,
|
|
2281
|
+
returnUrl,
|
|
2282
|
+
submitButtonText,
|
|
2283
|
+
buttonClassName,
|
|
2284
|
+
setElementKind,
|
|
2285
|
+
setPaymentInputComplete,
|
|
2286
|
+
setTermsAccepted,
|
|
2287
|
+
submit
|
|
2288
|
+
}),
|
|
2289
|
+
[
|
|
2290
|
+
planRef,
|
|
2291
|
+
productRef,
|
|
2292
|
+
prefillCustomer,
|
|
2293
|
+
resolvedPlanRef,
|
|
2294
|
+
plan,
|
|
2295
|
+
clientSecret,
|
|
2296
|
+
stripe,
|
|
2297
|
+
elements,
|
|
2298
|
+
isProcessing,
|
|
2299
|
+
isReady,
|
|
2300
|
+
paymentInputComplete,
|
|
2301
|
+
termsAccepted,
|
|
2302
|
+
requireTermsAcceptance,
|
|
2303
|
+
canSubmit,
|
|
2304
|
+
error,
|
|
2305
|
+
elementKind,
|
|
2306
|
+
returnUrl,
|
|
2307
|
+
submitButtonText,
|
|
2308
|
+
buttonClassName,
|
|
2309
|
+
submit
|
|
2310
|
+
]
|
|
2311
|
+
);
|
|
2312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PaymentFormProvider, { value: contextValue, children });
|
|
2313
|
+
};
|
|
2314
|
+
var FreeInner = ({
|
|
2315
|
+
planRef,
|
|
2316
|
+
productRef,
|
|
2317
|
+
plan,
|
|
2318
|
+
resolvedPlanRef,
|
|
2319
|
+
requireTermsAcceptance,
|
|
2320
|
+
submitButtonText,
|
|
2321
|
+
buttonClassName,
|
|
2322
|
+
onFreePlan,
|
|
2323
|
+
onResult,
|
|
2324
|
+
onError,
|
|
2325
|
+
children
|
|
2326
|
+
}) => {
|
|
2327
|
+
const copy = useCopy();
|
|
2328
|
+
const { refetch } = usePurchase();
|
|
2329
|
+
const { activate, state, error: activationError, result: activationResult } = useActivation();
|
|
2330
|
+
const [termsAccepted, setTermsAccepted] = (0, import_react16.useState)(false);
|
|
2331
|
+
const [localError, setLocalError] = (0, import_react16.useState)(null);
|
|
2332
|
+
const resultFiredRef = (0, import_react16.useRef)(false);
|
|
2333
|
+
(0, import_react16.useEffect)(() => {
|
|
2334
|
+
if (state === "activated" && activationResult && !resultFiredRef.current) {
|
|
2335
|
+
resultFiredRef.current = true;
|
|
2336
|
+
const res = { kind: "activated", result: activationResult };
|
|
2337
|
+
onResult?.(res);
|
|
2338
|
+
refetch().catch(() => {
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
}, [state, activationResult, onResult, refetch]);
|
|
2342
|
+
const isProcessing = state === "activating";
|
|
2343
|
+
const canSubmit = !isProcessing && (!requireTermsAcceptance || termsAccepted) && !!productRef;
|
|
2344
|
+
const submit = (0, import_react16.useCallback)(async () => {
|
|
2345
|
+
if (!productRef) {
|
|
2346
|
+
const msg = copy.errors.configMissingPlanOrProduct;
|
|
2347
|
+
setLocalError(msg);
|
|
2348
|
+
onError?.(new Error(msg));
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
setLocalError(null);
|
|
2352
|
+
try {
|
|
2353
|
+
if (onFreePlan) {
|
|
2354
|
+
await onFreePlan(plan);
|
|
2355
|
+
onResult?.({
|
|
2356
|
+
kind: "activated",
|
|
2357
|
+
result: {
|
|
2358
|
+
status: "activated",
|
|
2359
|
+
productRef,
|
|
2360
|
+
planRef: plan.reference
|
|
2361
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
await activate({ productRef, planRef: plan.reference });
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
const msg = err instanceof Error ? err.message : copy.activation.failed;
|
|
2369
|
+
setLocalError(msg);
|
|
2370
|
+
onError?.(err instanceof Error ? err : new Error(msg));
|
|
2371
|
+
}
|
|
2372
|
+
}, [productRef, plan, onFreePlan, activate, onResult, onError, copy]);
|
|
2373
|
+
const error = localError ?? activationError;
|
|
2374
|
+
const contextValue = (0, import_react16.useMemo)(
|
|
2375
|
+
() => ({
|
|
2376
|
+
planRef,
|
|
2377
|
+
productRef,
|
|
2378
|
+
prefillCustomer: void 0,
|
|
2379
|
+
resolvedPlanRef,
|
|
2380
|
+
plan,
|
|
2381
|
+
clientSecret: null,
|
|
2382
|
+
stripe: null,
|
|
2383
|
+
elements: null,
|
|
2384
|
+
isProcessing,
|
|
2385
|
+
isReady: true,
|
|
2386
|
+
paymentInputComplete: true,
|
|
2387
|
+
termsAccepted,
|
|
2388
|
+
requireTermsAcceptance,
|
|
2389
|
+
canSubmit,
|
|
2390
|
+
error,
|
|
2391
|
+
elementKind: null,
|
|
2392
|
+
returnUrl: "",
|
|
2393
|
+
submitButtonText,
|
|
2394
|
+
buttonClassName,
|
|
2395
|
+
setElementKind: () => {
|
|
2396
|
+
},
|
|
2397
|
+
setPaymentInputComplete: () => {
|
|
2398
|
+
},
|
|
2399
|
+
setTermsAccepted,
|
|
2400
|
+
submit
|
|
2401
|
+
}),
|
|
2402
|
+
[
|
|
2403
|
+
planRef,
|
|
2404
|
+
productRef,
|
|
2405
|
+
resolvedPlanRef,
|
|
2406
|
+
plan,
|
|
2407
|
+
isProcessing,
|
|
2408
|
+
termsAccepted,
|
|
2409
|
+
requireTermsAcceptance,
|
|
2410
|
+
canSubmit,
|
|
2411
|
+
error,
|
|
2412
|
+
submitButtonText,
|
|
2413
|
+
buttonClassName,
|
|
2414
|
+
submit
|
|
2415
|
+
]
|
|
2416
|
+
);
|
|
2417
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PaymentFormProvider, { value: contextValue, children });
|
|
2418
|
+
};
|
|
2419
|
+
var Summary = (props) => {
|
|
2420
|
+
const ctx = usePaymentForm();
|
|
2421
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2422
|
+
CheckoutSummary2,
|
|
2423
|
+
{
|
|
2424
|
+
...props,
|
|
2425
|
+
planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
|
|
2426
|
+
productRef: ctx.productRef
|
|
2427
|
+
}
|
|
2428
|
+
);
|
|
2429
|
+
};
|
|
2430
|
+
var MandateTextPrimitive = (props) => {
|
|
2431
|
+
const ctx = usePaymentForm();
|
|
2432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2433
|
+
MandateText,
|
|
2434
|
+
{
|
|
2435
|
+
...props,
|
|
2436
|
+
planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
|
|
2437
|
+
productRef: ctx.productRef
|
|
2438
|
+
}
|
|
2439
|
+
);
|
|
2440
|
+
};
|
|
2441
|
+
var CustomerFields = (0, import_react16.forwardRef)(
|
|
2442
|
+
function PaymentFormCustomerFields({ asChild, readOnly: _readOnly = true, children, ...rest }, ref) {
|
|
2443
|
+
const copy = useCopy();
|
|
2444
|
+
const customer = useCustomer();
|
|
2445
|
+
const { prefillCustomer } = usePaymentForm();
|
|
2446
|
+
const email = customer.email ?? prefillCustomer?.email;
|
|
2447
|
+
const name = customer.name ?? prefillCustomer?.name;
|
|
2448
|
+
if (!email && !name) return null;
|
|
2449
|
+
const Comp = asChild ? Slot : "div";
|
|
2450
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Comp, { ref, "data-solvapay-payment-form-customer-fields": "", ...rest, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
2451
|
+
email && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-solvapay-payment-form-customer-email": "", children: [
|
|
2452
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { children: [
|
|
2453
|
+
copy.customer.emailLabel,
|
|
2454
|
+
": "
|
|
2455
|
+
] }),
|
|
2456
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: email })
|
|
2457
|
+
] }),
|
|
2458
|
+
name && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { "data-solvapay-payment-form-customer-name": "", children: [
|
|
2459
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { children: [
|
|
2460
|
+
copy.customer.nameLabel,
|
|
2461
|
+
": "
|
|
2462
|
+
] }),
|
|
2463
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: name })
|
|
2464
|
+
] })
|
|
2465
|
+
] }) });
|
|
2466
|
+
}
|
|
2467
|
+
);
|
|
2468
|
+
var PaymentElementSlot = ({ options }) => {
|
|
2469
|
+
const { setElementKind, setPaymentInputComplete, isReady, stripe, elements } = usePaymentForm();
|
|
2470
|
+
const locale = useLocale();
|
|
2471
|
+
(0, import_react16.useEffect)(() => {
|
|
2472
|
+
if (stripe && elements) setElementKind("payment-element");
|
|
2473
|
+
}, [setElementKind, stripe, elements]);
|
|
2474
|
+
if (!stripe || !elements) return null;
|
|
2475
|
+
if (!isReady) {
|
|
2476
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, { size: "sm" }) });
|
|
2477
|
+
}
|
|
2478
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-payment-element": "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2479
|
+
import_react_stripe_js.PaymentElement,
|
|
2480
|
+
{
|
|
2481
|
+
options,
|
|
2482
|
+
onChange: (e) => setPaymentInputComplete(e.complete)
|
|
2483
|
+
},
|
|
2484
|
+
locale || "default"
|
|
2485
|
+
) });
|
|
2486
|
+
};
|
|
2487
|
+
var CardElementSlot = ({ options }) => {
|
|
2488
|
+
const { setElementKind, setPaymentInputComplete, isReady, stripe, elements } = usePaymentForm();
|
|
2489
|
+
(0, import_react16.useEffect)(() => {
|
|
2490
|
+
if (stripe && elements) setElementKind("card-element");
|
|
2491
|
+
}, [setElementKind, stripe, elements]);
|
|
2492
|
+
if (!stripe || !elements) return null;
|
|
2493
|
+
if (!isReady) {
|
|
2494
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-loading": "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, { size: "sm" }) });
|
|
2495
|
+
}
|
|
2496
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { "data-solvapay-payment-form-card-element": "", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2497
|
+
import_react_stripe_js.CardElement,
|
|
2498
|
+
{
|
|
2499
|
+
options,
|
|
2500
|
+
onChange: (e) => setPaymentInputComplete(e.complete)
|
|
2501
|
+
}
|
|
2502
|
+
) });
|
|
2503
|
+
};
|
|
2504
|
+
var TermsCheckbox = (0, import_react16.forwardRef)(
|
|
2505
|
+
function PaymentFormTermsCheckbox({ asChild, label, children, ...rest }, ref) {
|
|
2506
|
+
const { termsAccepted, setTermsAccepted } = usePaymentForm();
|
|
2507
|
+
const copy = useCopy();
|
|
2508
|
+
const id = "solvapay-terms-checkbox";
|
|
2509
|
+
if (asChild) {
|
|
2510
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Slot, { ref, "data-solvapay-payment-form-terms": "", ...rest, children });
|
|
2511
|
+
}
|
|
2512
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
2513
|
+
"label",
|
|
2514
|
+
{
|
|
2515
|
+
ref,
|
|
2516
|
+
htmlFor: id,
|
|
2517
|
+
"data-solvapay-payment-form-terms": "",
|
|
2518
|
+
...rest,
|
|
2519
|
+
children: [
|
|
2520
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2521
|
+
"input",
|
|
2522
|
+
{
|
|
2523
|
+
id,
|
|
2524
|
+
type: "checkbox",
|
|
2525
|
+
checked: termsAccepted,
|
|
2526
|
+
onChange: (e) => setTermsAccepted(e.target.checked)
|
|
2527
|
+
}
|
|
2528
|
+
),
|
|
2529
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: label ?? copy.terms.checkboxLabel })
|
|
2530
|
+
]
|
|
2531
|
+
}
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
);
|
|
2535
|
+
var SubmitButton = (0, import_react16.forwardRef)(
|
|
2536
|
+
function PaymentFormSubmitButton({ asChild, onClick, children, ...rest }, ref) {
|
|
2537
|
+
const ctx = usePaymentForm();
|
|
2538
|
+
const copy = useCopy();
|
|
2539
|
+
const locale = useLocale();
|
|
2540
|
+
const { plan } = usePlan({
|
|
2541
|
+
planRef: ctx.planRef || ctx.resolvedPlanRef || void 0,
|
|
2542
|
+
productRef: ctx.productRef
|
|
2543
|
+
});
|
|
2544
|
+
const { product } = useProduct(ctx.productRef);
|
|
2545
|
+
const variant = deriveVariant(plan ?? ctx.plan ?? void 0);
|
|
2546
|
+
const dataVariant = toSubmitVariant(variant);
|
|
2547
|
+
const dataState = ctx.isProcessing ? "processing" : !ctx.canSubmit ? "disabled" : "idle";
|
|
2548
|
+
const amountFormatted = formatPrice(
|
|
2549
|
+
plan?.price ?? ctx.plan?.price ?? 0,
|
|
2550
|
+
plan?.currency ?? ctx.plan?.currency ?? "usd",
|
|
2551
|
+
{ locale, free: copy.interval.free }
|
|
2552
|
+
);
|
|
2553
|
+
const label = resolveCta({
|
|
2554
|
+
variant,
|
|
2555
|
+
plan: plan ?? ctx.plan,
|
|
2556
|
+
product,
|
|
2557
|
+
amountFormatted,
|
|
2558
|
+
copy,
|
|
2559
|
+
override: typeof children === "string" ? children : ctx.submitButtonText
|
|
2560
|
+
});
|
|
2561
|
+
const content = ctx.isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
2562
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, { size: "sm" }),
|
|
2563
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: copy.cta.processing })
|
|
2564
|
+
] }) : children && typeof children !== "string" ? children : label;
|
|
2565
|
+
const buttonProps = {
|
|
2566
|
+
"data-solvapay-payment-form-submit": "",
|
|
2567
|
+
"data-state": dataState,
|
|
2568
|
+
"data-variant": dataVariant,
|
|
2569
|
+
"aria-busy": ctx.isProcessing,
|
|
2570
|
+
"aria-disabled": !ctx.canSubmit,
|
|
2571
|
+
"aria-label": label,
|
|
2572
|
+
disabled: !ctx.canSubmit,
|
|
2573
|
+
className: rest.className ?? ctx.buttonClassName,
|
|
2574
|
+
onClick: composeEventHandlers(onClick, (e) => {
|
|
2575
|
+
e.preventDefault();
|
|
2576
|
+
ctx.submit();
|
|
2577
|
+
}),
|
|
2578
|
+
...rest
|
|
2579
|
+
};
|
|
2580
|
+
if (asChild) {
|
|
2581
|
+
return (
|
|
2582
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2583
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Slot, { ref, ...buttonProps, children: content })
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { ref, type: "submit", ...buttonProps, children: content });
|
|
2587
|
+
}
|
|
2588
|
+
);
|
|
2589
|
+
var Loading2 = (0, import_react16.forwardRef)(function PaymentFormLoading({ asChild, children, ...rest }, ref) {
|
|
2590
|
+
const ctx = usePaymentForm();
|
|
2591
|
+
if (ctx.isReady && ctx.clientSecret) return null;
|
|
2592
|
+
const Comp = asChild ? Slot : "div";
|
|
2593
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Comp, { ref, "data-solvapay-payment-form-loading": "", ...rest, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Spinner, { size: "sm" }) });
|
|
2594
|
+
});
|
|
2595
|
+
var ErrorSlot2 = (0, import_react16.forwardRef)(function PaymentFormError({ asChild, children, ...rest }, ref) {
|
|
2596
|
+
const ctx = usePaymentForm();
|
|
2597
|
+
if (!ctx.error) return null;
|
|
2598
|
+
const Comp = asChild ? Slot : "div";
|
|
2599
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
2600
|
+
Comp,
|
|
2601
|
+
{
|
|
2602
|
+
ref,
|
|
2603
|
+
role: "alert",
|
|
2604
|
+
"aria-live": "assertive",
|
|
2605
|
+
"aria-atomic": "true",
|
|
2606
|
+
"data-solvapay-payment-form-error": "",
|
|
2607
|
+
...rest,
|
|
2608
|
+
children: children ?? ctx.error
|
|
2609
|
+
}
|
|
2610
|
+
);
|
|
2611
|
+
});
|
|
2612
|
+
var PaymentFormRoot2 = Root3;
|
|
2613
|
+
var PaymentFormSummary = Summary;
|
|
2614
|
+
var PaymentFormCustomerFields2 = CustomerFields;
|
|
2615
|
+
var PaymentFormPaymentElement = PaymentElementSlot;
|
|
2616
|
+
var PaymentFormCardElement = CardElementSlot;
|
|
2617
|
+
var PaymentFormMandateText = MandateTextPrimitive;
|
|
2618
|
+
var PaymentFormTermsCheckbox2 = TermsCheckbox;
|
|
2619
|
+
var PaymentFormSubmitButton2 = SubmitButton;
|
|
2620
|
+
var PaymentFormLoading2 = Loading2;
|
|
2621
|
+
var PaymentFormError2 = ErrorSlot2;
|
|
2622
|
+
var PaymentForm = {
|
|
2623
|
+
Root: Root3,
|
|
2624
|
+
Summary,
|
|
2625
|
+
CustomerFields,
|
|
2626
|
+
PaymentElement: PaymentElementSlot,
|
|
2627
|
+
CardElement: CardElementSlot,
|
|
2628
|
+
MandateText: MandateTextPrimitive,
|
|
2629
|
+
TermsCheckbox,
|
|
2630
|
+
SubmitButton,
|
|
2631
|
+
Loading: Loading2,
|
|
2632
|
+
Error: ErrorSlot2
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
// src/primitives/ActivationFlow.tsx
|
|
2636
|
+
var import_react20 = require("react");
|
|
2637
|
+
|
|
2638
|
+
// src/primitives/AmountPicker.tsx
|
|
2639
|
+
var import_react19 = require("react");
|
|
2640
|
+
|
|
2641
|
+
// src/hooks/useTopupAmountSelector.ts
|
|
2642
|
+
var import_react17 = require("react");
|
|
2643
|
+
function getQuickAmounts(currency) {
|
|
2644
|
+
switch (currency.toUpperCase()) {
|
|
2645
|
+
case "SEK":
|
|
2646
|
+
case "NOK":
|
|
2647
|
+
case "DKK":
|
|
2648
|
+
return [100, 500, 1e3, 5e3];
|
|
2649
|
+
case "JPY":
|
|
2650
|
+
return [1e3, 5e3, 1e4, 5e4];
|
|
2651
|
+
case "KRW":
|
|
2652
|
+
return [1e4, 5e4, 1e5, 5e5];
|
|
2653
|
+
case "ISK":
|
|
2654
|
+
case "HUF":
|
|
2655
|
+
return [1e3, 5e3, 1e4, 5e4];
|
|
2656
|
+
default:
|
|
2657
|
+
return [10, 50, 100, 500];
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
function getCurrencySymbol(currency) {
|
|
2661
|
+
try {
|
|
2662
|
+
const parts = new Intl.NumberFormat("en", { style: "currency", currency }).formatToParts(0);
|
|
2663
|
+
const sym = parts.find((p) => p.type === "currency");
|
|
2664
|
+
return sym?.value || currency;
|
|
2665
|
+
} catch {
|
|
2666
|
+
return currency;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
var DEFAULT_MIN = 1;
|
|
2670
|
+
var DEFAULT_MAX = 1e5;
|
|
2671
|
+
function useTopupAmountSelector(options) {
|
|
2672
|
+
const { currency, minAmount = DEFAULT_MIN, maxAmount = DEFAULT_MAX } = options;
|
|
2673
|
+
const copy = useCopy();
|
|
2674
|
+
const [selectedAmount, setSelectedAmount] = (0, import_react17.useState)(null);
|
|
2675
|
+
const [customAmount, setCustomAmountRaw] = (0, import_react17.useState)("");
|
|
2676
|
+
const [error, setError] = (0, import_react17.useState)(null);
|
|
2677
|
+
const quickAmounts = (0, import_react17.useMemo)(() => getQuickAmounts(currency), [currency]);
|
|
2678
|
+
const currencySymbol = (0, import_react17.useMemo)(() => getCurrencySymbol(currency), [currency]);
|
|
2679
|
+
const resolvedAmount = (0, import_react17.useMemo)(() => {
|
|
2680
|
+
if (customAmount) {
|
|
2681
|
+
const parsed = parseFloat(customAmount);
|
|
2682
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
2683
|
+
}
|
|
2684
|
+
return selectedAmount;
|
|
2685
|
+
}, [selectedAmount, customAmount]);
|
|
2686
|
+
const selectQuickAmount = (0, import_react17.useCallback)(
|
|
2687
|
+
(amount) => {
|
|
2688
|
+
setSelectedAmount(amount);
|
|
2689
|
+
setCustomAmountRaw("");
|
|
2690
|
+
setError(null);
|
|
2691
|
+
},
|
|
2692
|
+
[]
|
|
2693
|
+
);
|
|
2694
|
+
const setCustomAmount = (0, import_react17.useCallback)(
|
|
2695
|
+
(value) => {
|
|
2696
|
+
const sanitized = value.replace(/[^0-9.]/g, "");
|
|
2697
|
+
const dotIndex = sanitized.indexOf(".");
|
|
2698
|
+
const cleaned = dotIndex === -1 ? sanitized : sanitized.slice(0, dotIndex + 1) + sanitized.slice(dotIndex + 1).replace(/\./g, "");
|
|
2699
|
+
setCustomAmountRaw(cleaned);
|
|
2700
|
+
setSelectedAmount(null);
|
|
2701
|
+
setError(null);
|
|
2702
|
+
},
|
|
2703
|
+
[]
|
|
2704
|
+
);
|
|
2705
|
+
const validate = (0, import_react17.useCallback)(() => {
|
|
2706
|
+
if (resolvedAmount == null) {
|
|
2707
|
+
setError(copy.topup.selectOrEnterAmount);
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
if (resolvedAmount < minAmount) {
|
|
2711
|
+
setError(
|
|
2712
|
+
interpolate(copy.topup.minAmount, {
|
|
2713
|
+
amount: `${currencySymbol}${minAmount}`
|
|
2714
|
+
})
|
|
2715
|
+
);
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
if (resolvedAmount > maxAmount) {
|
|
2719
|
+
setError(
|
|
2720
|
+
interpolate(copy.topup.maxAmount, {
|
|
2721
|
+
amount: `${currencySymbol}${maxAmount.toLocaleString()}`
|
|
2722
|
+
})
|
|
2723
|
+
);
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
setError(null);
|
|
2727
|
+
return true;
|
|
2728
|
+
}, [resolvedAmount, minAmount, maxAmount, currencySymbol, copy]);
|
|
2729
|
+
const reset = (0, import_react17.useCallback)(() => {
|
|
2730
|
+
setSelectedAmount(null);
|
|
2731
|
+
setCustomAmountRaw("");
|
|
2732
|
+
setError(null);
|
|
2733
|
+
}, []);
|
|
2734
|
+
return {
|
|
2735
|
+
quickAmounts,
|
|
2736
|
+
selectedAmount,
|
|
2737
|
+
customAmount,
|
|
2738
|
+
resolvedAmount,
|
|
2739
|
+
selectQuickAmount,
|
|
2740
|
+
setCustomAmount,
|
|
2741
|
+
error,
|
|
2742
|
+
validate,
|
|
2743
|
+
reset,
|
|
2744
|
+
currencySymbol
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// src/hooks/useBalance.ts
|
|
2749
|
+
var import_react18 = require("react");
|
|
2750
|
+
function useBalance() {
|
|
2751
|
+
const { balance } = useSolvaPay();
|
|
2752
|
+
(0, import_react18.useEffect)(() => {
|
|
2753
|
+
balance.refetch();
|
|
2754
|
+
}, [balance.refetch]);
|
|
2755
|
+
return balance;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// src/primitives/AmountPicker.tsx
|
|
2759
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
2760
|
+
var AmountPickerContext = (0, import_react19.createContext)(null);
|
|
2761
|
+
function usePickerCtx(part) {
|
|
2762
|
+
const ctx = (0, import_react19.useContext)(AmountPickerContext);
|
|
2763
|
+
if (!ctx) {
|
|
2764
|
+
throw new Error(`AmountPicker.${part} must be rendered inside <AmountPicker.Root>.`);
|
|
2765
|
+
}
|
|
2766
|
+
return ctx;
|
|
2767
|
+
}
|
|
2768
|
+
var Root4 = (0, import_react19.forwardRef)(function AmountPickerRoot({ currency, minAmount, maxAmount, onChange, asChild, children, ...rest }, forwardedRef) {
|
|
2769
|
+
const solva = (0, import_react19.useContext)(SolvaPayContext);
|
|
2770
|
+
if (!solva) throw new MissingProviderError("AmountPicker");
|
|
2771
|
+
const selector = useTopupAmountSelector({ currency, minAmount, maxAmount });
|
|
2772
|
+
const { creditsPerMinorUnit, displayExchangeRate } = useBalance();
|
|
2773
|
+
(0, import_react19.useEffect)(() => {
|
|
2774
|
+
onChange?.(selector.resolvedAmount);
|
|
2775
|
+
}, [selector.resolvedAmount, onChange]);
|
|
2776
|
+
const resolvedAmountMinor = selector.resolvedAmount != null && selector.resolvedAmount > 0 ? Math.round(selector.resolvedAmount * 100) : null;
|
|
2777
|
+
const rate = displayExchangeRate ?? 1;
|
|
2778
|
+
const estimatedCredits = creditsPerMinorUnit != null && creditsPerMinorUnit > 0 && resolvedAmountMinor != null ? Math.floor(resolvedAmountMinor / rate * creditsPerMinorUnit) : null;
|
|
2779
|
+
const isApproximate = rate !== 1;
|
|
2780
|
+
const ctx = (0, import_react19.useMemo)(
|
|
2781
|
+
() => ({
|
|
2782
|
+
...selector,
|
|
2783
|
+
currency,
|
|
2784
|
+
creditsPerMinorUnit,
|
|
2785
|
+
displayExchangeRate,
|
|
2786
|
+
estimatedCredits,
|
|
2787
|
+
isApproximate
|
|
2788
|
+
}),
|
|
2789
|
+
[selector, currency, creditsPerMinorUnit, displayExchangeRate, estimatedCredits, isApproximate]
|
|
2790
|
+
);
|
|
2791
|
+
const Comp = asChild ? Slot : "div";
|
|
2792
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(AmountPickerContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Comp, { ref: forwardedRef, "data-solvapay-amount-picker": "", ...rest, children }) });
|
|
2793
|
+
});
|
|
2794
|
+
var Option = (0, import_react19.forwardRef)(function AmountPickerOption({ asChild, amount, onClick, children, ...rest }, forwardedRef) {
|
|
2795
|
+
const ctx = usePickerCtx("Option");
|
|
2796
|
+
const isActive = ctx.selectedAmount === amount && !ctx.customAmount;
|
|
2797
|
+
const state = rest.disabled ? "disabled" : isActive ? "selected" : "idle";
|
|
2798
|
+
const commonProps = {
|
|
2799
|
+
"data-solvapay-amount-picker-option": "",
|
|
2800
|
+
"data-state": state,
|
|
2801
|
+
"data-amount": String(amount),
|
|
2802
|
+
"aria-pressed": isActive,
|
|
2803
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
2804
|
+
ctx.selectQuickAmount(amount);
|
|
2805
|
+
}),
|
|
2806
|
+
...rest
|
|
2807
|
+
};
|
|
2808
|
+
if (asChild) {
|
|
2809
|
+
return (
|
|
2810
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2811
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
2812
|
+
ctx.currencySymbol,
|
|
2813
|
+
amount.toLocaleString()
|
|
2814
|
+
] }) })
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
2818
|
+
ctx.currencySymbol,
|
|
2819
|
+
amount.toLocaleString()
|
|
2820
|
+
] }) });
|
|
2821
|
+
});
|
|
2822
|
+
var Custom = (0, import_react19.forwardRef)(function AmountPickerCustom({ asChild, onChange, onFocus, ...rest }, forwardedRef) {
|
|
2823
|
+
const ctx = usePickerCtx("Custom");
|
|
2824
|
+
const state = ctx.customAmount ? "active" : "dormant";
|
|
2825
|
+
const commonProps = {
|
|
2826
|
+
"data-solvapay-amount-picker-custom": "",
|
|
2827
|
+
"data-state": state,
|
|
2828
|
+
type: "text",
|
|
2829
|
+
inputMode: "decimal",
|
|
2830
|
+
placeholder: "0.00",
|
|
2831
|
+
value: ctx.customAmount,
|
|
2832
|
+
onChange: composeEventHandlers(onChange, (e) => {
|
|
2833
|
+
ctx.setCustomAmount(e.target.value);
|
|
2834
|
+
}),
|
|
2835
|
+
onFocus: composeEventHandlers(onFocus, () => {
|
|
2836
|
+
if (ctx.selectedAmount != null) ctx.selectQuickAmount(0);
|
|
2837
|
+
}),
|
|
2838
|
+
...rest
|
|
2839
|
+
};
|
|
2840
|
+
if (asChild) {
|
|
2841
|
+
return (
|
|
2842
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2843
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps })
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("input", { ref: forwardedRef, ...commonProps });
|
|
2847
|
+
});
|
|
2848
|
+
var Confirm = (0, import_react19.forwardRef)(function AmountPickerConfirm({ asChild, onClick, onConfirm, disabled, children, ...rest }, forwardedRef) {
|
|
2849
|
+
const ctx = usePickerCtx("Confirm");
|
|
2850
|
+
const isDisabled = disabled || !ctx.resolvedAmount;
|
|
2851
|
+
const handleConfirm = (0, import_react19.useCallback)(() => {
|
|
2852
|
+
if (ctx.validate() && ctx.resolvedAmount != null) {
|
|
2853
|
+
onConfirm?.(ctx.resolvedAmount);
|
|
2854
|
+
}
|
|
2855
|
+
}, [ctx, onConfirm]);
|
|
2856
|
+
const commonProps = {
|
|
2857
|
+
"data-solvapay-amount-picker-confirm": "",
|
|
2858
|
+
"data-state": isDisabled ? "disabled" : "idle",
|
|
2859
|
+
disabled: isDisabled,
|
|
2860
|
+
"aria-disabled": isDisabled || void 0,
|
|
2861
|
+
onClick: composeEventHandlers(onClick, handleConfirm),
|
|
2862
|
+
...rest
|
|
2863
|
+
};
|
|
2864
|
+
if (asChild) {
|
|
2865
|
+
return (
|
|
2866
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2867
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Slot, { ref: forwardedRef, ...commonProps, children })
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children });
|
|
2871
|
+
});
|
|
2872
|
+
var AmountPickerRoot2 = Root4;
|
|
2873
|
+
var AmountPickerOption2 = Option;
|
|
2874
|
+
var AmountPickerCustom2 = Custom;
|
|
2875
|
+
var AmountPickerConfirm2 = Confirm;
|
|
2876
|
+
var AmountPicker = {
|
|
2877
|
+
Root: Root4,
|
|
2878
|
+
Option,
|
|
2879
|
+
Custom,
|
|
2880
|
+
Confirm
|
|
2881
|
+
};
|
|
2882
|
+
function useAmountPicker() {
|
|
2883
|
+
return usePickerCtx("useAmountPicker");
|
|
2884
|
+
}
|
|
2885
|
+
function useAmountPickerCopy() {
|
|
2886
|
+
const copy = useCopy();
|
|
2887
|
+
const ctx = usePickerCtx("useAmountPickerCopy");
|
|
2888
|
+
return {
|
|
2889
|
+
selectAmountLabel: copy.amountPicker.selectAmountLabel,
|
|
2890
|
+
customAmountLabel: copy.amountPicker.customAmountLabel,
|
|
2891
|
+
creditEstimate: (credits) => interpolate(
|
|
2892
|
+
ctx.isApproximate ? copy.amountPicker.creditEstimateApprox : copy.amountPicker.creditEstimateExact,
|
|
2893
|
+
{ credits: new Intl.NumberFormat().format(credits) }
|
|
2894
|
+
)
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// src/primitives/ActivationFlow.tsx
|
|
2899
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
2900
|
+
var ActivationFlowContext = (0, import_react20.createContext)(null);
|
|
2901
|
+
function useFlowCtx(part) {
|
|
2902
|
+
const ctx = (0, import_react20.useContext)(ActivationFlowContext);
|
|
2903
|
+
if (!ctx) {
|
|
2904
|
+
throw new Error(`ActivationFlow.${part} must be rendered inside <ActivationFlow.Root>.`);
|
|
2905
|
+
}
|
|
2906
|
+
return ctx;
|
|
2907
|
+
}
|
|
2908
|
+
var Root5 = (0, import_react20.forwardRef)(function ActivationFlowRoot({
|
|
2909
|
+
productRef,
|
|
2910
|
+
planRef: planRefProp,
|
|
2911
|
+
onSuccess,
|
|
2912
|
+
onError,
|
|
2913
|
+
retryDelayMs = 1500,
|
|
2914
|
+
retryBackoffMs = 2e3,
|
|
2915
|
+
asChild,
|
|
2916
|
+
children,
|
|
2917
|
+
...rest
|
|
2918
|
+
}, forwardedRef) {
|
|
2919
|
+
const solva = (0, import_react20.useContext)(SolvaPayContext);
|
|
2920
|
+
if (!solva) throw new MissingProviderError("ActivationFlow");
|
|
2921
|
+
if (!productRef) throw new MissingProductRefError("ActivationFlow");
|
|
2922
|
+
const planSelection = usePlanSelection();
|
|
2923
|
+
const resolvedPlanRef = planRefProp ?? planSelection?.selectedPlanRef ?? void 0;
|
|
2924
|
+
const { plan } = usePlan({ planRef: resolvedPlanRef, productRef });
|
|
2925
|
+
const { activate, state, error, result, reset } = useActivation();
|
|
2926
|
+
const currency = plan?.currency ?? "USD";
|
|
2927
|
+
const amountSelector = useTopupAmountSelector({ currency });
|
|
2928
|
+
const { adjustBalance, creditsPerMinorUnit, displayExchangeRate } = useBalance();
|
|
2929
|
+
const [step, setStep] = (0, import_react20.useState)("summary");
|
|
2930
|
+
const calledSuccessRef = (0, import_react20.useRef)(false);
|
|
2931
|
+
const retryTimeoutRef = (0, import_react20.useRef)(null);
|
|
2932
|
+
(0, import_react20.useEffect)(() => {
|
|
2933
|
+
if (state === "activated" && !calledSuccessRef.current) {
|
|
2934
|
+
calledSuccessRef.current = true;
|
|
2935
|
+
setStep("activated");
|
|
2936
|
+
if (result) {
|
|
2937
|
+
const activationResult = { kind: "activated", result };
|
|
2938
|
+
onSuccess?.(activationResult);
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
}, [state, result, onSuccess]);
|
|
2942
|
+
(0, import_react20.useEffect)(() => {
|
|
2943
|
+
if (state === "topup_required" && (step === "summary" || step === "activating")) {
|
|
2944
|
+
setStep("selectAmount");
|
|
2945
|
+
}
|
|
2946
|
+
}, [state, step]);
|
|
2947
|
+
(0, import_react20.useEffect)(() => {
|
|
2948
|
+
if ((state === "error" || state === "payment_required") && error) {
|
|
2949
|
+
setStep("error");
|
|
2950
|
+
if (state === "error") onError?.(new Error(error));
|
|
2951
|
+
}
|
|
2952
|
+
}, [state, error, onError]);
|
|
2953
|
+
const handleActivate = (0, import_react20.useCallback)(async () => {
|
|
2954
|
+
if (!resolvedPlanRef) return;
|
|
2955
|
+
setStep("activating");
|
|
2956
|
+
await activate({ productRef, planRef: resolvedPlanRef });
|
|
2957
|
+
}, [activate, productRef, resolvedPlanRef]);
|
|
2958
|
+
const goToTopupPayment = (0, import_react20.useCallback)(() => {
|
|
2959
|
+
if (amountSelector.validate()) setStep("topupPayment");
|
|
2960
|
+
}, [amountSelector]);
|
|
2961
|
+
const backToSelectAmount = (0, import_react20.useCallback)(() => {
|
|
2962
|
+
setStep("selectAmount");
|
|
2963
|
+
}, []);
|
|
2964
|
+
const retryActivation = (0, import_react20.useCallback)(async () => {
|
|
2965
|
+
if (!resolvedPlanRef) return;
|
|
2966
|
+
setStep("retrying");
|
|
2967
|
+
const amountMinor = Math.round((amountSelector.resolvedAmount || 0) * 100);
|
|
2968
|
+
const rate = displayExchangeRate ?? 1;
|
|
2969
|
+
if (creditsPerMinorUnit) {
|
|
2970
|
+
adjustBalance(Math.floor(amountMinor / rate * creditsPerMinorUnit));
|
|
2971
|
+
}
|
|
2972
|
+
await new Promise((r) => setTimeout(r, retryDelayMs));
|
|
2973
|
+
await activate({ productRef, planRef: resolvedPlanRef });
|
|
2974
|
+
}, [
|
|
2975
|
+
resolvedPlanRef,
|
|
2976
|
+
amountSelector.resolvedAmount,
|
|
2977
|
+
displayExchangeRate,
|
|
2978
|
+
creditsPerMinorUnit,
|
|
2979
|
+
adjustBalance,
|
|
2980
|
+
retryDelayMs,
|
|
2981
|
+
activate,
|
|
2982
|
+
productRef
|
|
2983
|
+
]);
|
|
2984
|
+
const handleTopupSuccess = (0, import_react20.useCallback)(async () => {
|
|
2985
|
+
await retryActivation();
|
|
2986
|
+
}, [retryActivation]);
|
|
2987
|
+
(0, import_react20.useEffect)(() => {
|
|
2988
|
+
if (step !== "retrying") return;
|
|
2989
|
+
if (state !== "topup_required") return;
|
|
2990
|
+
if (!resolvedPlanRef) return;
|
|
2991
|
+
retryTimeoutRef.current = setTimeout(async () => {
|
|
2992
|
+
await activate({ productRef, planRef: resolvedPlanRef });
|
|
2993
|
+
}, retryBackoffMs);
|
|
2994
|
+
return () => {
|
|
2995
|
+
if (retryTimeoutRef.current) clearTimeout(retryTimeoutRef.current);
|
|
2996
|
+
};
|
|
2997
|
+
}, [step, state, resolvedPlanRef, retryBackoffMs, activate, productRef]);
|
|
2998
|
+
const resetFlow = (0, import_react20.useCallback)(() => {
|
|
2999
|
+
reset();
|
|
3000
|
+
calledSuccessRef.current = false;
|
|
3001
|
+
setStep("summary");
|
|
3002
|
+
}, [reset]);
|
|
3003
|
+
const amountCents = Math.round((amountSelector.resolvedAmount || 0) * 100);
|
|
3004
|
+
const ctx = (0, import_react20.useMemo)(
|
|
3005
|
+
() => ({
|
|
3006
|
+
step,
|
|
3007
|
+
plan,
|
|
3008
|
+
productRef,
|
|
3009
|
+
planRef: resolvedPlanRef ?? "",
|
|
3010
|
+
amountSelector,
|
|
3011
|
+
amountCents,
|
|
3012
|
+
currency,
|
|
3013
|
+
activate: handleActivate,
|
|
3014
|
+
reset: resetFlow,
|
|
3015
|
+
goToTopupPayment,
|
|
3016
|
+
backToSelectAmount,
|
|
3017
|
+
onTopupSuccess: handleTopupSuccess,
|
|
3018
|
+
error,
|
|
3019
|
+
onError
|
|
3020
|
+
}),
|
|
3021
|
+
[
|
|
3022
|
+
step,
|
|
3023
|
+
plan,
|
|
3024
|
+
productRef,
|
|
3025
|
+
resolvedPlanRef,
|
|
3026
|
+
amountSelector,
|
|
3027
|
+
amountCents,
|
|
3028
|
+
currency,
|
|
3029
|
+
handleActivate,
|
|
3030
|
+
resetFlow,
|
|
3031
|
+
goToTopupPayment,
|
|
3032
|
+
backToSelectAmount,
|
|
3033
|
+
handleTopupSuccess,
|
|
3034
|
+
error,
|
|
3035
|
+
onError
|
|
3036
|
+
]
|
|
3037
|
+
);
|
|
3038
|
+
const Comp = asChild ? Slot : "div";
|
|
3039
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ActivationFlowContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
3040
|
+
Comp,
|
|
3041
|
+
{
|
|
3042
|
+
ref: forwardedRef,
|
|
3043
|
+
"data-solvapay-activation-flow": "",
|
|
3044
|
+
"data-state": step,
|
|
3045
|
+
...rest,
|
|
3046
|
+
children
|
|
3047
|
+
}
|
|
3048
|
+
) });
|
|
3049
|
+
});
|
|
3050
|
+
function matchStep(step, allowed) {
|
|
3051
|
+
return allowed.includes(step);
|
|
3052
|
+
}
|
|
3053
|
+
var Summary2 = (0, import_react20.forwardRef)(function ActivationFlowSummary({ asChild, children, ...rest }, forwardedRef) {
|
|
3054
|
+
const ctx = useFlowCtx("Summary");
|
|
3055
|
+
if (!matchStep(ctx.step, ["summary", "activating"])) return null;
|
|
3056
|
+
const Comp = asChild ? Slot : "div";
|
|
3057
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Comp, { ref: forwardedRef, "data-solvapay-activation-flow-summary": "", ...rest, children });
|
|
3058
|
+
});
|
|
3059
|
+
var ActivateButton = (0, import_react20.forwardRef)(
|
|
3060
|
+
function ActivationFlowActivateButton({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
3061
|
+
const ctx = useFlowCtx("ActivateButton");
|
|
3062
|
+
const copy = useCopy();
|
|
3063
|
+
if (!matchStep(ctx.step, ["summary", "activating"])) return null;
|
|
3064
|
+
const isActivating = ctx.step === "activating";
|
|
3065
|
+
const disabled = isActivating;
|
|
3066
|
+
const commonProps = {
|
|
3067
|
+
"data-solvapay-activation-flow-activate": "",
|
|
3068
|
+
"data-state": isActivating ? "activating" : "idle",
|
|
3069
|
+
"aria-busy": isActivating,
|
|
3070
|
+
disabled,
|
|
3071
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
3072
|
+
void ctx.activate();
|
|
3073
|
+
}),
|
|
3074
|
+
...rest
|
|
3075
|
+
};
|
|
3076
|
+
const label = isActivating ? copy.activationFlow.activatingLabel : copy.activationFlow.activateButton;
|
|
3077
|
+
if (asChild) {
|
|
3078
|
+
return (
|
|
3079
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3080
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: label }) })
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? label });
|
|
3084
|
+
}
|
|
3085
|
+
);
|
|
3086
|
+
var AmountPickerMount = ({ children }) => {
|
|
3087
|
+
const ctx = useFlowCtx("AmountPicker");
|
|
3088
|
+
if (ctx.step !== "selectAmount") return null;
|
|
3089
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { "data-solvapay-activation-flow-amount-picker": "", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(AmountPicker.Root, { currency: ctx.currency, children }) });
|
|
3090
|
+
};
|
|
3091
|
+
var ContinueButton = (0, import_react20.forwardRef)(
|
|
3092
|
+
function ActivationFlowContinueButton({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
3093
|
+
const ctx = useFlowCtx("ContinueButton");
|
|
3094
|
+
const copy = useCopy();
|
|
3095
|
+
if (ctx.step !== "selectAmount") return null;
|
|
3096
|
+
const disabled = !ctx.amountSelector.resolvedAmount;
|
|
3097
|
+
const commonProps = {
|
|
3098
|
+
"data-solvapay-activation-flow-continue": "",
|
|
3099
|
+
"data-state": disabled ? "disabled" : "idle",
|
|
3100
|
+
disabled,
|
|
3101
|
+
"aria-disabled": disabled || void 0,
|
|
3102
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
3103
|
+
ctx.goToTopupPayment();
|
|
3104
|
+
}),
|
|
3105
|
+
...rest
|
|
3106
|
+
};
|
|
3107
|
+
if (asChild) {
|
|
3108
|
+
return (
|
|
3109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3110
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, { children: copy.activationFlow.continueToPayment }) })
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? copy.activationFlow.continueToPayment });
|
|
3114
|
+
}
|
|
3115
|
+
);
|
|
3116
|
+
var Retrying = (0, import_react20.forwardRef)(function ActivationFlowRetrying({ asChild, children, ...rest }, forwardedRef) {
|
|
3117
|
+
const ctx = useFlowCtx("Retrying");
|
|
3118
|
+
if (ctx.step !== "retrying") return null;
|
|
3119
|
+
const Comp = asChild ? Slot : "div";
|
|
3120
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Comp, { ref: forwardedRef, "data-solvapay-activation-flow-retrying": "", ...rest, children });
|
|
3121
|
+
});
|
|
3122
|
+
var Activated = (0, import_react20.forwardRef)(function ActivationFlowActivated({ asChild, children, ...rest }, forwardedRef) {
|
|
3123
|
+
const ctx = useFlowCtx("Activated");
|
|
3124
|
+
if (ctx.step !== "activated") return null;
|
|
3125
|
+
const Comp = asChild ? Slot : "div";
|
|
3126
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Comp, { ref: forwardedRef, "data-solvapay-activation-flow-activated": "", ...rest, children });
|
|
3127
|
+
});
|
|
3128
|
+
var Loading3 = (0, import_react20.forwardRef)(function ActivationFlowLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
3129
|
+
const ctx = useFlowCtx("Loading");
|
|
3130
|
+
if (ctx.plan) return null;
|
|
3131
|
+
const Comp = asChild ? Slot : "div";
|
|
3132
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Comp, { ref: forwardedRef, "data-solvapay-activation-flow-loading": "", ...rest, children });
|
|
3133
|
+
});
|
|
3134
|
+
var ErrorSlot3 = (0, import_react20.forwardRef)(function ActivationFlowError({ asChild, children, ...rest }, forwardedRef) {
|
|
3135
|
+
const ctx = useFlowCtx("Error");
|
|
3136
|
+
if (ctx.step !== "error") return null;
|
|
3137
|
+
const Comp = asChild ? Slot : "div";
|
|
3138
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
3139
|
+
Comp,
|
|
3140
|
+
{
|
|
3141
|
+
ref: forwardedRef,
|
|
3142
|
+
role: "alert",
|
|
3143
|
+
"data-solvapay-activation-flow-error": "",
|
|
3144
|
+
...rest,
|
|
3145
|
+
children: children ?? ctx.error
|
|
3146
|
+
}
|
|
3147
|
+
);
|
|
3148
|
+
});
|
|
3149
|
+
var ActivationFlowRoot2 = Root5;
|
|
3150
|
+
var ActivationFlowSummary2 = Summary2;
|
|
3151
|
+
var ActivationFlowActivateButton2 = ActivateButton;
|
|
3152
|
+
var ActivationFlowAmountPicker = AmountPickerMount;
|
|
3153
|
+
var ActivationFlowContinueButton2 = ContinueButton;
|
|
3154
|
+
var ActivationFlowRetrying2 = Retrying;
|
|
3155
|
+
var ActivationFlowActivated2 = Activated;
|
|
3156
|
+
var ActivationFlowLoading2 = Loading3;
|
|
3157
|
+
var ActivationFlowError2 = ErrorSlot3;
|
|
3158
|
+
var ActivationFlow = {
|
|
3159
|
+
Root: Root5,
|
|
3160
|
+
Summary: Summary2,
|
|
3161
|
+
ActivateButton,
|
|
3162
|
+
AmountPicker: AmountPickerMount,
|
|
3163
|
+
ContinueButton,
|
|
3164
|
+
Retrying,
|
|
3165
|
+
Activated,
|
|
3166
|
+
Loading: Loading3,
|
|
3167
|
+
Error: ErrorSlot3
|
|
3168
|
+
};
|
|
3169
|
+
function useActivationFlow() {
|
|
3170
|
+
return useFlowCtx("useActivationFlow");
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// src/primitives/BalanceBadge.tsx
|
|
3174
|
+
var import_react21 = require("react");
|
|
3175
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
3176
|
+
var BalanceBadge = (0, import_react21.forwardRef)(function BalanceBadge2({ asChild, numberOnly, lowThreshold = 10, children, ...rest }, forwardedRef) {
|
|
3177
|
+
const solva = (0, import_react21.useContext)(SolvaPayContext);
|
|
3178
|
+
if (!solva) throw new MissingProviderError("BalanceBadge");
|
|
3179
|
+
const { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate, loading } = useBalance();
|
|
3180
|
+
const copy = useCopy();
|
|
3181
|
+
const locale = useLocale();
|
|
3182
|
+
const state = loading ? "loading" : credits == null || credits === 0 ? "zero" : credits <= lowThreshold ? "low" : "ok";
|
|
3183
|
+
const commonProps = {
|
|
3184
|
+
"data-solvapay-balance-badge": "",
|
|
3185
|
+
"data-state": state,
|
|
3186
|
+
"data-credits": credits == null ? void 0 : String(credits),
|
|
3187
|
+
"aria-busy": loading,
|
|
3188
|
+
...rest
|
|
3189
|
+
};
|
|
3190
|
+
if (state === "loading") {
|
|
3191
|
+
if (asChild) {
|
|
3192
|
+
return (
|
|
3193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3194
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_jsx_runtime14.Fragment, {}) })
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { ref: forwardedRef, ...commonProps });
|
|
3198
|
+
}
|
|
3199
|
+
if (credits == null) return null;
|
|
3200
|
+
const formattedCredits = new Intl.NumberFormat(locale).format(credits);
|
|
3201
|
+
let currencyEquivalent = "";
|
|
3202
|
+
if (!numberOnly && displayCurrency && creditsPerMinorUnit) {
|
|
3203
|
+
const usdCents = credits / creditsPerMinorUnit;
|
|
3204
|
+
const displayMajorUnits = usdCents * (displayExchangeRate ?? 1) / 100;
|
|
3205
|
+
const formatted = new Intl.NumberFormat(locale, {
|
|
3206
|
+
style: "currency",
|
|
3207
|
+
currency: displayCurrency,
|
|
3208
|
+
minimumFractionDigits: 2
|
|
3209
|
+
}).format(displayMajorUnits);
|
|
3210
|
+
currencyEquivalent = interpolate(copy.balance.currencyEquivalent, { amount: formatted });
|
|
3211
|
+
}
|
|
3212
|
+
const content = children ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
|
|
3213
|
+
formattedCredits,
|
|
3214
|
+
!numberOnly && copy.balance.credits,
|
|
3215
|
+
!numberOnly && currencyEquivalent
|
|
3216
|
+
] });
|
|
3217
|
+
if (asChild) {
|
|
3218
|
+
return (
|
|
3219
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3220
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: content })
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { ref: forwardedRef, ...commonProps, children: content });
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
// src/primitives/CancelPlanButton.tsx
|
|
3227
|
+
var import_react23 = require("react");
|
|
3228
|
+
|
|
3229
|
+
// src/hooks/usePurchaseActions.ts
|
|
3230
|
+
var import_react22 = require("react");
|
|
3231
|
+
function usePurchaseActions() {
|
|
3232
|
+
const ctx = useSolvaPay();
|
|
3233
|
+
const [isCancelling, setIsCancelling] = (0, import_react22.useState)(false);
|
|
3234
|
+
const [isReactivating, setIsReactivating] = (0, import_react22.useState)(false);
|
|
3235
|
+
const [isActivating, setIsActivating] = (0, import_react22.useState)(false);
|
|
3236
|
+
const cancelRenewal = (0, import_react22.useCallback)(
|
|
3237
|
+
async (params) => {
|
|
3238
|
+
setIsCancelling(true);
|
|
3239
|
+
try {
|
|
3240
|
+
return await ctx.cancelRenewal(params);
|
|
3241
|
+
} finally {
|
|
3242
|
+
setIsCancelling(false);
|
|
3243
|
+
}
|
|
3244
|
+
},
|
|
3245
|
+
[ctx.cancelRenewal]
|
|
3246
|
+
);
|
|
3247
|
+
const reactivateRenewal = (0, import_react22.useCallback)(
|
|
3248
|
+
async (params) => {
|
|
3249
|
+
setIsReactivating(true);
|
|
3250
|
+
try {
|
|
3251
|
+
return await ctx.reactivateRenewal(params);
|
|
3252
|
+
} finally {
|
|
3253
|
+
setIsReactivating(false);
|
|
3254
|
+
}
|
|
3255
|
+
},
|
|
3256
|
+
[ctx.reactivateRenewal]
|
|
3257
|
+
);
|
|
3258
|
+
const activatePlan = (0, import_react22.useCallback)(
|
|
3259
|
+
async (params) => {
|
|
3260
|
+
setIsActivating(true);
|
|
3261
|
+
try {
|
|
3262
|
+
return await ctx.activatePlan(params);
|
|
3263
|
+
} finally {
|
|
3264
|
+
setIsActivating(false);
|
|
3265
|
+
}
|
|
3266
|
+
},
|
|
3267
|
+
[ctx.activatePlan]
|
|
3268
|
+
);
|
|
3269
|
+
return {
|
|
3270
|
+
cancelRenewal,
|
|
3271
|
+
reactivateRenewal,
|
|
3272
|
+
activatePlan,
|
|
3273
|
+
isCancelling,
|
|
3274
|
+
isReactivating,
|
|
3275
|
+
isActivating
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// src/primitives/CancelPlanButton.tsx
|
|
3280
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
3281
|
+
function resolveConfirmText(confirm, purchase, defaults) {
|
|
3282
|
+
if (confirm === false) return null;
|
|
3283
|
+
if (typeof confirm === "string") return confirm;
|
|
3284
|
+
const planType = purchase?.planSnapshot?.planType;
|
|
3285
|
+
if (planType === "usage-based") return defaults.usageBased;
|
|
3286
|
+
return defaults.recurring;
|
|
3287
|
+
}
|
|
3288
|
+
var CancelPlanButton = (0, import_react23.forwardRef)(
|
|
3289
|
+
function CancelPlanButton2({
|
|
3290
|
+
asChild,
|
|
3291
|
+
purchaseRef,
|
|
3292
|
+
reason,
|
|
3293
|
+
confirm = true,
|
|
3294
|
+
onCancelled,
|
|
3295
|
+
onError,
|
|
3296
|
+
onClick,
|
|
3297
|
+
children,
|
|
3298
|
+
...rest
|
|
3299
|
+
}, forwardedRef) {
|
|
3300
|
+
const solva = (0, import_react23.useContext)(SolvaPayContext);
|
|
3301
|
+
if (!solva) throw new MissingProviderError("CancelPlanButton");
|
|
3302
|
+
const copy = useCopy();
|
|
3303
|
+
const { activePurchase } = usePurchase();
|
|
3304
|
+
const { cancelRenewal, isCancelling } = usePurchaseActions();
|
|
3305
|
+
const effectivePurchase = purchaseRef ? activePurchase && activePurchase.reference === purchaseRef ? activePurchase : { reference: purchaseRef } : activePurchase;
|
|
3306
|
+
const effectiveRef = purchaseRef ?? effectivePurchase?.reference;
|
|
3307
|
+
const cancel = (0, import_react23.useCallback)(async () => {
|
|
3308
|
+
if (!effectiveRef) return;
|
|
3309
|
+
const confirmText = resolveConfirmText(confirm, effectivePurchase, {
|
|
3310
|
+
recurring: copy.cancelPlan.confirmRecurring,
|
|
3311
|
+
usageBased: copy.cancelPlan.confirmUsageBased
|
|
3312
|
+
});
|
|
3313
|
+
if (confirmText && typeof window !== "undefined" && !window.confirm(confirmText)) {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
try {
|
|
3317
|
+
await cancelRenewal({ purchaseRef: effectiveRef, reason });
|
|
3318
|
+
onCancelled?.();
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
3321
|
+
}
|
|
3322
|
+
}, [effectiveRef, effectivePurchase, confirm, copy, cancelRenewal, reason, onCancelled, onError]);
|
|
3323
|
+
const disabled = isCancelling || !effectiveRef;
|
|
3324
|
+
const state = isCancelling ? "cancelling" : "idle";
|
|
3325
|
+
const commonProps = {
|
|
3326
|
+
"data-solvapay-cancel-plan": "",
|
|
3327
|
+
"data-state": state,
|
|
3328
|
+
"aria-busy": isCancelling,
|
|
3329
|
+
"aria-disabled": disabled || void 0,
|
|
3330
|
+
disabled,
|
|
3331
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
3332
|
+
void cancel();
|
|
3333
|
+
}),
|
|
3334
|
+
...rest
|
|
3335
|
+
};
|
|
3336
|
+
const label = isCancelling ? copy.cancelPlan.buttonLoading : copy.cancelPlan.button;
|
|
3337
|
+
if (asChild) {
|
|
3338
|
+
return (
|
|
3339
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3340
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_jsx_runtime15.Fragment, { children: label }) })
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? label });
|
|
3344
|
+
}
|
|
3345
|
+
);
|
|
3346
|
+
|
|
3347
|
+
// src/primitives/CancelledPlanNotice.tsx
|
|
3348
|
+
var import_react25 = require("react");
|
|
3349
|
+
|
|
3350
|
+
// src/hooks/usePurchaseStatus.ts
|
|
3351
|
+
var import_react24 = require("react");
|
|
3352
|
+
function usePurchaseStatus() {
|
|
3353
|
+
const { purchases } = usePurchase();
|
|
3354
|
+
const locale = useLocale();
|
|
3355
|
+
const isPaidPurchase = (0, import_react24.useCallback)((p) => {
|
|
3356
|
+
return (p.amount ?? 0) > 0;
|
|
3357
|
+
}, []);
|
|
3358
|
+
const purchaseData = (0, import_react24.useMemo)(() => {
|
|
3359
|
+
const cancelledPaidPurchases = purchases.filter((p) => {
|
|
3360
|
+
return p.status === "active" && p.cancelledAt && isPaidPurchase(p);
|
|
3361
|
+
});
|
|
3362
|
+
const cancelledPurchase = cancelledPaidPurchases.sort(
|
|
3363
|
+
(a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
|
|
3364
|
+
)[0] || null;
|
|
3365
|
+
const shouldShowCancelledNotice = !!cancelledPurchase;
|
|
3366
|
+
return {
|
|
3367
|
+
cancelledPurchase,
|
|
3368
|
+
shouldShowCancelledNotice
|
|
3369
|
+
};
|
|
3370
|
+
}, [purchases, isPaidPurchase]);
|
|
3371
|
+
const formatDate = (0, import_react24.useCallback)(
|
|
3372
|
+
(dateString) => {
|
|
3373
|
+
if (!dateString) return null;
|
|
3374
|
+
return new Date(dateString).toLocaleDateString(locale || "en-US", {
|
|
3375
|
+
year: "numeric",
|
|
3376
|
+
month: "long",
|
|
3377
|
+
day: "numeric"
|
|
3378
|
+
});
|
|
3379
|
+
},
|
|
3380
|
+
[locale]
|
|
3381
|
+
);
|
|
3382
|
+
const getDaysUntilExpiration = (0, import_react24.useCallback)((endDate) => {
|
|
3383
|
+
if (!endDate) return null;
|
|
3384
|
+
const now = /* @__PURE__ */ new Date();
|
|
3385
|
+
const expiration = new Date(endDate);
|
|
3386
|
+
const diffTime = expiration.getTime() - now.getTime();
|
|
3387
|
+
const diffDays = Math.ceil(diffTime / (1e3 * 60 * 60 * 24));
|
|
3388
|
+
return diffDays > 0 ? diffDays : 0;
|
|
3389
|
+
}, []);
|
|
3390
|
+
return {
|
|
3391
|
+
cancelledPurchase: purchaseData.cancelledPurchase,
|
|
3392
|
+
shouldShowCancelledNotice: purchaseData.shouldShowCancelledNotice,
|
|
3393
|
+
formatDate,
|
|
3394
|
+
getDaysUntilExpiration
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// src/primitives/CancelledPlanNotice.tsx
|
|
3399
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
3400
|
+
var CancelledPlanNoticeContext = (0, import_react25.createContext)(null);
|
|
3401
|
+
function useNoticeCtx(part) {
|
|
3402
|
+
const ctx = (0, import_react25.useContext)(CancelledPlanNoticeContext);
|
|
3403
|
+
if (!ctx) {
|
|
3404
|
+
throw new Error(
|
|
3405
|
+
`CancelledPlanNotice.${part} must be rendered inside <CancelledPlanNotice.Root>.`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
return ctx;
|
|
3409
|
+
}
|
|
3410
|
+
var Root6 = (0, import_react25.forwardRef)(function CancelledPlanNoticeRoot({ onReactivated, onError, asChild, children, ...rest }, forwardedRef) {
|
|
3411
|
+
const solva = (0, import_react25.useContext)(SolvaPayContext);
|
|
3412
|
+
if (!solva) throw new MissingProviderError("CancelledPlanNotice");
|
|
3413
|
+
const {
|
|
3414
|
+
cancelledPurchase,
|
|
3415
|
+
shouldShowCancelledNotice,
|
|
3416
|
+
formatDate,
|
|
3417
|
+
getDaysUntilExpiration
|
|
3418
|
+
} = usePurchaseStatus();
|
|
3419
|
+
const { reactivateRenewal, isReactivating } = usePurchaseActions();
|
|
3420
|
+
const reactivate = (0, import_react25.useCallback)(async () => {
|
|
3421
|
+
if (!cancelledPurchase) return;
|
|
3422
|
+
try {
|
|
3423
|
+
await reactivateRenewal({ purchaseRef: cancelledPurchase.reference });
|
|
3424
|
+
onReactivated?.();
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
3427
|
+
}
|
|
3428
|
+
}, [cancelledPurchase, reactivateRenewal, onReactivated, onError]);
|
|
3429
|
+
const daysRemaining = (0, import_react25.useMemo)(
|
|
3430
|
+
() => cancelledPurchase ? getDaysUntilExpiration(cancelledPurchase.endDate) : null,
|
|
3431
|
+
[cancelledPurchase, getDaysUntilExpiration]
|
|
3432
|
+
);
|
|
3433
|
+
const ctx = (0, import_react25.useMemo)(() => {
|
|
3434
|
+
if (!cancelledPurchase) return null;
|
|
3435
|
+
const state = daysRemaining != null && daysRemaining > 0 ? "active" : "expired";
|
|
3436
|
+
return {
|
|
3437
|
+
purchase: cancelledPurchase,
|
|
3438
|
+
state,
|
|
3439
|
+
daysRemaining,
|
|
3440
|
+
hasReason: !!cancelledPurchase.cancellationReason,
|
|
3441
|
+
formatDate,
|
|
3442
|
+
reactivate,
|
|
3443
|
+
isReactivating
|
|
3444
|
+
};
|
|
3445
|
+
}, [cancelledPurchase, daysRemaining, formatDate, reactivate, isReactivating]);
|
|
3446
|
+
if (!shouldShowCancelledNotice || !ctx) return null;
|
|
3447
|
+
const Comp = asChild ? Slot : "div";
|
|
3448
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CancelledPlanNoticeContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3449
|
+
Comp,
|
|
3450
|
+
{
|
|
3451
|
+
ref: forwardedRef,
|
|
3452
|
+
"data-solvapay-cancelled-notice": "",
|
|
3453
|
+
"data-state": ctx.state,
|
|
3454
|
+
"data-has-reason": ctx.hasReason ? "" : void 0,
|
|
3455
|
+
...rest,
|
|
3456
|
+
children
|
|
3457
|
+
}
|
|
3458
|
+
) });
|
|
3459
|
+
});
|
|
3460
|
+
var Heading2 = (0, import_react25.forwardRef)(function CancelledPlanNoticeHeading({ asChild, children, ...rest }, forwardedRef) {
|
|
3461
|
+
useNoticeCtx("Heading");
|
|
3462
|
+
const copy = useCopy();
|
|
3463
|
+
const Comp = asChild ? Slot : "p";
|
|
3464
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-heading": "", ...rest, children: children ?? copy.cancelledNotice.heading });
|
|
3465
|
+
});
|
|
3466
|
+
var Expires = (0, import_react25.forwardRef)(function CancelledPlanNoticeExpires({ asChild, children, ...rest }, forwardedRef) {
|
|
3467
|
+
const ctx = useNoticeCtx("Expires");
|
|
3468
|
+
const copy = useCopy();
|
|
3469
|
+
const date = ctx.formatDate(ctx.purchase.endDate);
|
|
3470
|
+
if (!ctx.purchase.endDate) {
|
|
3471
|
+
const Comp2 = asChild ? Slot : "p";
|
|
3472
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp2, { ref: forwardedRef, "data-solvapay-cancelled-notice-expires": "", ...rest, children: children ?? copy.cancelledNotice.accessEnded });
|
|
3473
|
+
}
|
|
3474
|
+
const Comp = asChild ? Slot : "p";
|
|
3475
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-expires": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.expiresLabel, { date: date ?? "" }) });
|
|
3476
|
+
});
|
|
3477
|
+
var DaysRemaining = (0, import_react25.forwardRef)(
|
|
3478
|
+
function CancelledPlanNoticeDaysRemaining({ asChild, children, ...rest }, forwardedRef) {
|
|
3479
|
+
const ctx = useNoticeCtx("DaysRemaining");
|
|
3480
|
+
const copy = useCopy();
|
|
3481
|
+
if (ctx.daysRemaining == null || ctx.daysRemaining <= 0) return null;
|
|
3482
|
+
const template = ctx.daysRemaining === 1 ? copy.cancelledNotice.dayRemaining : copy.cancelledNotice.daysRemaining;
|
|
3483
|
+
const Comp = asChild ? Slot : "p";
|
|
3484
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-days-remaining": "", ...rest, children: children ?? interpolate(template, { days: ctx.daysRemaining }) });
|
|
3485
|
+
}
|
|
3486
|
+
);
|
|
3487
|
+
var AccessUntil = (0, import_react25.forwardRef)(
|
|
3488
|
+
function CancelledPlanNoticeAccessUntil({ asChild, children, ...rest }, forwardedRef) {
|
|
3489
|
+
const ctx = useNoticeCtx("AccessUntil");
|
|
3490
|
+
const copy = useCopy();
|
|
3491
|
+
if (!ctx.purchase.endDate) return null;
|
|
3492
|
+
const Comp = asChild ? Slot : "p";
|
|
3493
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-access-until": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.accessUntil, {
|
|
3494
|
+
product: ctx.purchase.productName
|
|
3495
|
+
}) });
|
|
3496
|
+
}
|
|
3497
|
+
);
|
|
3498
|
+
var CancelledOn = (0, import_react25.forwardRef)(
|
|
3499
|
+
function CancelledPlanNoticeCancelledOn({ asChild, children, ...rest }, forwardedRef) {
|
|
3500
|
+
const ctx = useNoticeCtx("CancelledOn");
|
|
3501
|
+
const copy = useCopy();
|
|
3502
|
+
if (!ctx.purchase.cancelledAt) return null;
|
|
3503
|
+
const date = ctx.formatDate(ctx.purchase.cancelledAt);
|
|
3504
|
+
const Comp = asChild ? Slot : "p";
|
|
3505
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-cancelled-on": "", ...rest, children: children ?? interpolate(copy.cancelledNotice.cancelledOn, { date: date ?? "" }) });
|
|
3506
|
+
}
|
|
3507
|
+
);
|
|
3508
|
+
var Reason = (0, import_react25.forwardRef)(function CancelledPlanNoticeReason({ asChild, children, ...rest }, forwardedRef) {
|
|
3509
|
+
const ctx = useNoticeCtx("Reason");
|
|
3510
|
+
if (!ctx.hasReason) return null;
|
|
3511
|
+
const Comp = asChild ? Slot : "span";
|
|
3512
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Comp, { ref: forwardedRef, "data-solvapay-cancelled-notice-reason": "", ...rest, children: children ?? ctx.purchase.cancellationReason });
|
|
3513
|
+
});
|
|
3514
|
+
var ReactivateButton = (0, import_react25.forwardRef)(
|
|
3515
|
+
function CancelledPlanNoticeReactivateButton({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
3516
|
+
const ctx = useNoticeCtx("ReactivateButton");
|
|
3517
|
+
const copy = useCopy();
|
|
3518
|
+
const disabled = ctx.isReactivating;
|
|
3519
|
+
const commonProps = {
|
|
3520
|
+
"data-solvapay-cancelled-notice-reactivate": "",
|
|
3521
|
+
"data-state": ctx.isReactivating ? "reactivating" : "idle",
|
|
3522
|
+
"aria-busy": ctx.isReactivating,
|
|
3523
|
+
"aria-disabled": disabled || void 0,
|
|
3524
|
+
disabled,
|
|
3525
|
+
onClick: composeEventHandlers(onClick, () => {
|
|
3526
|
+
void ctx.reactivate();
|
|
3527
|
+
}),
|
|
3528
|
+
...rest
|
|
3529
|
+
};
|
|
3530
|
+
const label = ctx.isReactivating ? copy.cancelledNotice.reactivateButtonLoading : copy.cancelledNotice.reactivateButton;
|
|
3531
|
+
if (asChild) {
|
|
3532
|
+
return (
|
|
3533
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3534
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_jsx_runtime16.Fragment, { children: label }) })
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { ref: forwardedRef, type: "button", ...commonProps, children: children ?? label });
|
|
3538
|
+
}
|
|
3539
|
+
);
|
|
3540
|
+
var CancelledPlanNoticeRoot2 = Root6;
|
|
3541
|
+
var CancelledPlanNoticeHeading2 = Heading2;
|
|
3542
|
+
var CancelledPlanNoticeExpires2 = Expires;
|
|
3543
|
+
var CancelledPlanNoticeDaysRemaining2 = DaysRemaining;
|
|
3544
|
+
var CancelledPlanNoticeAccessUntil2 = AccessUntil;
|
|
3545
|
+
var CancelledPlanNoticeCancelledOn2 = CancelledOn;
|
|
3546
|
+
var CancelledPlanNoticeReason2 = Reason;
|
|
3547
|
+
var CancelledPlanNoticeReactivateButton2 = ReactivateButton;
|
|
3548
|
+
var CancelledPlanNotice = {
|
|
3549
|
+
Root: Root6,
|
|
3550
|
+
Heading: Heading2,
|
|
3551
|
+
Expires,
|
|
3552
|
+
DaysRemaining,
|
|
3553
|
+
AccessUntil,
|
|
3554
|
+
CancelledOn,
|
|
3555
|
+
Reason,
|
|
3556
|
+
ReactivateButton
|
|
3557
|
+
};
|
|
3558
|
+
function useCancelledPlanNotice() {
|
|
3559
|
+
return useNoticeCtx("useCancelledPlanNotice");
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
// src/primitives/CreditGate.tsx
|
|
3563
|
+
var import_react28 = require("react");
|
|
3564
|
+
|
|
3565
|
+
// src/primitives/TopupForm.tsx
|
|
3566
|
+
var import_react27 = require("react");
|
|
3567
|
+
var import_react_stripe_js2 = require("@stripe/react-stripe-js");
|
|
3568
|
+
|
|
3569
|
+
// src/hooks/useTopup.ts
|
|
3570
|
+
var import_react26 = require("react");
|
|
3571
|
+
var import_stripe_js2 = require("@stripe/stripe-js");
|
|
3572
|
+
var stripePromiseCache2 = /* @__PURE__ */ new Map();
|
|
3573
|
+
function getStripeCacheKey2(publishableKey, accountId) {
|
|
3574
|
+
return accountId ? `${publishableKey}:${accountId}` : publishableKey;
|
|
3575
|
+
}
|
|
3576
|
+
function useTopup(options) {
|
|
3577
|
+
const { amount, currency } = options;
|
|
3578
|
+
const { createTopupPayment, customerRef, updateCustomerRef } = useSolvaPay();
|
|
3579
|
+
const [loading, setLoading] = (0, import_react26.useState)(false);
|
|
3580
|
+
const [error, setError] = (0, import_react26.useState)(null);
|
|
3581
|
+
const [stripePromise, setStripePromise] = (0, import_react26.useState)(null);
|
|
3582
|
+
const [clientSecret, setClientSecret] = (0, import_react26.useState)(null);
|
|
3583
|
+
const isStartingRef = (0, import_react26.useRef)(false);
|
|
3584
|
+
const startTopup = (0, import_react26.useCallback)(async () => {
|
|
3585
|
+
if (isStartingRef.current || loading) {
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (!amount || amount <= 0) {
|
|
3589
|
+
setError(new Error("useTopup: amount must be a positive number"));
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
isStartingRef.current = true;
|
|
3593
|
+
setLoading(true);
|
|
3594
|
+
setError(null);
|
|
3595
|
+
try {
|
|
3596
|
+
const result = await createTopupPayment({ amount, currency });
|
|
3597
|
+
if (!result || typeof result !== "object") {
|
|
3598
|
+
throw new Error("Invalid topup payment intent response from server");
|
|
3599
|
+
}
|
|
3600
|
+
if (!result.clientSecret || typeof result.clientSecret !== "string") {
|
|
3601
|
+
throw new Error("Invalid client secret in topup payment intent response");
|
|
3602
|
+
}
|
|
3603
|
+
if (!result.publishableKey || typeof result.publishableKey !== "string") {
|
|
3604
|
+
throw new Error("Invalid publishable key in topup payment intent response");
|
|
3605
|
+
}
|
|
3606
|
+
if (result.customerRef && result.customerRef !== customerRef && updateCustomerRef) {
|
|
3607
|
+
updateCustomerRef(result.customerRef);
|
|
3608
|
+
}
|
|
3609
|
+
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
3610
|
+
const cacheKey = getStripeCacheKey2(result.publishableKey, result.accountId);
|
|
3611
|
+
let stripe = stripePromiseCache2.get(cacheKey);
|
|
3612
|
+
if (!stripe) {
|
|
3613
|
+
stripe = (0, import_stripe_js2.loadStripe)(result.publishableKey, stripeOptions);
|
|
3614
|
+
stripePromiseCache2.set(cacheKey, stripe);
|
|
3615
|
+
}
|
|
3616
|
+
setStripePromise(stripe);
|
|
3617
|
+
setClientSecret(result.clientSecret);
|
|
3618
|
+
} catch (err) {
|
|
3619
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start topup");
|
|
3620
|
+
setError(error2);
|
|
3621
|
+
} finally {
|
|
3622
|
+
setLoading(false);
|
|
3623
|
+
isStartingRef.current = false;
|
|
3624
|
+
}
|
|
3625
|
+
}, [amount, currency, createTopupPayment, customerRef, updateCustomerRef, loading]);
|
|
3626
|
+
const reset = (0, import_react26.useCallback)(() => {
|
|
3627
|
+
isStartingRef.current = false;
|
|
3628
|
+
setLoading(false);
|
|
3629
|
+
setError(null);
|
|
3630
|
+
setStripePromise(null);
|
|
3631
|
+
setClientSecret(null);
|
|
3632
|
+
}, []);
|
|
3633
|
+
return {
|
|
3634
|
+
loading,
|
|
3635
|
+
error,
|
|
3636
|
+
stripePromise,
|
|
3637
|
+
clientSecret,
|
|
3638
|
+
startTopup,
|
|
3639
|
+
reset
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
// src/primitives/TopupForm.tsx
|
|
3644
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
3645
|
+
var TopupFormContext = (0, import_react27.createContext)(null);
|
|
3646
|
+
function useTopupCtx(part) {
|
|
3647
|
+
const ctx = (0, import_react27.useContext)(TopupFormContext);
|
|
3648
|
+
if (!ctx) {
|
|
3649
|
+
throw new Error(`TopupForm.${part} must be rendered inside <TopupForm.Root>.`);
|
|
3650
|
+
}
|
|
3651
|
+
return ctx;
|
|
3652
|
+
}
|
|
3653
|
+
var Root7 = (0, import_react27.forwardRef)(function TopupFormRoot(props, forwardedRef) {
|
|
3654
|
+
const {
|
|
3655
|
+
amount,
|
|
3656
|
+
currency,
|
|
3657
|
+
onSuccess,
|
|
3658
|
+
onError,
|
|
3659
|
+
returnUrl,
|
|
3660
|
+
submitButtonText: _submitButtonText,
|
|
3661
|
+
buttonClassName: _buttonClassName,
|
|
3662
|
+
className,
|
|
3663
|
+
asChild,
|
|
3664
|
+
children,
|
|
3665
|
+
...rest
|
|
3666
|
+
} = props;
|
|
3667
|
+
const solva = (0, import_react27.useContext)(SolvaPayContext);
|
|
3668
|
+
if (!solva) throw new MissingProviderError("TopupForm");
|
|
3669
|
+
const copy = useCopy();
|
|
3670
|
+
const locale = useLocale();
|
|
3671
|
+
const { loading, error: topupError, clientSecret, startTopup, stripePromise } = useTopup({
|
|
3672
|
+
amount,
|
|
3673
|
+
currency
|
|
3674
|
+
});
|
|
3675
|
+
const hasInitializedRef = (0, import_react27.useRef)(false);
|
|
3676
|
+
const hasAmount = amount > 0;
|
|
3677
|
+
(0, import_react27.useEffect)(() => {
|
|
3678
|
+
if (!hasInitializedRef.current && hasAmount && !loading && !topupError && !clientSecret) {
|
|
3679
|
+
hasInitializedRef.current = true;
|
|
3680
|
+
startTopup().catch(() => {
|
|
3681
|
+
hasInitializedRef.current = false;
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
if (hasAmount && clientSecret) hasInitializedRef.current = true;
|
|
3685
|
+
}, [hasAmount, loading, topupError, clientSecret, startTopup]);
|
|
3686
|
+
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
3687
|
+
const elementsOptions = (0, import_react27.useMemo)(() => {
|
|
3688
|
+
if (!clientSecret) return void 0;
|
|
3689
|
+
return { clientSecret, locale };
|
|
3690
|
+
}, [clientSecret, locale]);
|
|
3691
|
+
const Comp = asChild ? Slot : "div";
|
|
3692
|
+
const outerError = !hasAmount ? copy.errors.configMissingAmount : topupError ? `${copy.errors.topupInitFailed} ${topupError.message || copy.errors.unknownError}` : null;
|
|
3693
|
+
const dataState = outerError ? "error" : clientSecret && stripePromise ? "ready" : "loading";
|
|
3694
|
+
const canMountElements = !!(stripePromise && clientSecret && elementsOptions);
|
|
3695
|
+
const innerCommon = {
|
|
3696
|
+
amount,
|
|
3697
|
+
currency,
|
|
3698
|
+
clientSecret,
|
|
3699
|
+
returnUrl: finalReturnUrl,
|
|
3700
|
+
outerError,
|
|
3701
|
+
state: dataState,
|
|
3702
|
+
onSuccess,
|
|
3703
|
+
onError
|
|
3704
|
+
};
|
|
3705
|
+
const shell = /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3706
|
+
Comp,
|
|
3707
|
+
{
|
|
3708
|
+
ref: forwardedRef,
|
|
3709
|
+
className,
|
|
3710
|
+
"data-solvapay-topup-form": "",
|
|
3711
|
+
"data-state": dataState,
|
|
3712
|
+
...rest,
|
|
3713
|
+
children
|
|
3714
|
+
}
|
|
3715
|
+
);
|
|
3716
|
+
if (canMountElements) {
|
|
3717
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_stripe_js2.Elements, { stripe: stripePromise, options: elementsOptions, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Inner, { ...innerCommon, children: shell }) }, clientSecret);
|
|
3718
|
+
}
|
|
3719
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(OfflineInner, { ...innerCommon, children: shell });
|
|
3720
|
+
});
|
|
3721
|
+
var Inner = ({
|
|
3722
|
+
amount,
|
|
3723
|
+
currency,
|
|
3724
|
+
clientSecret,
|
|
3725
|
+
returnUrl,
|
|
3726
|
+
outerError,
|
|
3727
|
+
state,
|
|
3728
|
+
onSuccess,
|
|
3729
|
+
onError,
|
|
3730
|
+
children
|
|
3731
|
+
}) => {
|
|
3732
|
+
const stripe = (0, import_react_stripe_js2.useStripe)();
|
|
3733
|
+
const elements = (0, import_react_stripe_js2.useElements)();
|
|
3734
|
+
const copy = useCopy();
|
|
3735
|
+
const [paymentInputComplete, setPaymentInputComplete] = (0, import_react27.useState)(false);
|
|
3736
|
+
const [isProcessing, setIsProcessing] = (0, import_react27.useState)(false);
|
|
3737
|
+
const [error, setError] = (0, import_react27.useState)(null);
|
|
3738
|
+
const isReady = !!(stripe && elements);
|
|
3739
|
+
const canSubmit = isReady && paymentInputComplete && !isProcessing && !!clientSecret;
|
|
3740
|
+
const submit = (0, import_react27.useCallback)(async () => {
|
|
3741
|
+
if (!stripe || !elements || !clientSecret) {
|
|
3742
|
+
const msg = copy.errors.stripeUnavailable;
|
|
3743
|
+
setError(msg);
|
|
3744
|
+
onError?.(new Error(msg));
|
|
3745
|
+
return;
|
|
3746
|
+
}
|
|
3747
|
+
setError(null);
|
|
3748
|
+
setIsProcessing(true);
|
|
3749
|
+
const { error: confirmError, paymentIntent } = await stripe.confirmPayment({
|
|
3750
|
+
elements,
|
|
3751
|
+
clientSecret,
|
|
3752
|
+
confirmParams: { return_url: returnUrl },
|
|
3753
|
+
redirect: "if_required"
|
|
3754
|
+
});
|
|
3755
|
+
if (confirmError) {
|
|
3756
|
+
const msg = confirmError.message || copy.errors.paymentUnexpected;
|
|
3757
|
+
setError(msg);
|
|
3758
|
+
setIsProcessing(false);
|
|
3759
|
+
onError?.(new Error(msg));
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
setIsProcessing(false);
|
|
3763
|
+
if (paymentIntent) {
|
|
3764
|
+
await onSuccess?.(paymentIntent);
|
|
3765
|
+
}
|
|
3766
|
+
}, [stripe, elements, clientSecret, returnUrl, copy, onSuccess, onError]);
|
|
3767
|
+
const effectiveError = error ?? outerError;
|
|
3768
|
+
const ctx = (0, import_react27.useMemo)(
|
|
3769
|
+
() => ({
|
|
3770
|
+
amount,
|
|
3771
|
+
currency,
|
|
3772
|
+
state,
|
|
3773
|
+
clientSecret,
|
|
3774
|
+
stripe: stripe ?? null,
|
|
3775
|
+
elements: elements ?? null,
|
|
3776
|
+
isReady,
|
|
3777
|
+
isProcessing,
|
|
3778
|
+
paymentInputComplete,
|
|
3779
|
+
canSubmit,
|
|
3780
|
+
error: effectiveError,
|
|
3781
|
+
returnUrl,
|
|
3782
|
+
setPaymentInputComplete,
|
|
3783
|
+
submit
|
|
3784
|
+
}),
|
|
3785
|
+
[
|
|
3786
|
+
amount,
|
|
3787
|
+
currency,
|
|
3788
|
+
state,
|
|
3789
|
+
clientSecret,
|
|
3790
|
+
stripe,
|
|
3791
|
+
elements,
|
|
3792
|
+
isReady,
|
|
3793
|
+
isProcessing,
|
|
3794
|
+
paymentInputComplete,
|
|
3795
|
+
canSubmit,
|
|
3796
|
+
effectiveError,
|
|
3797
|
+
returnUrl,
|
|
3798
|
+
submit
|
|
3799
|
+
]
|
|
3800
|
+
);
|
|
3801
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TopupFormContext.Provider, { value: ctx, children });
|
|
3802
|
+
};
|
|
3803
|
+
var OfflineInner = ({
|
|
3804
|
+
amount,
|
|
3805
|
+
currency,
|
|
3806
|
+
clientSecret,
|
|
3807
|
+
returnUrl,
|
|
3808
|
+
outerError,
|
|
3809
|
+
state,
|
|
3810
|
+
children
|
|
3811
|
+
}) => {
|
|
3812
|
+
const noopSubmit = (0, import_react27.useCallback)(async () => {
|
|
3813
|
+
}, []);
|
|
3814
|
+
const noopSet = (0, import_react27.useCallback)(() => {
|
|
3815
|
+
}, []);
|
|
3816
|
+
const ctx = (0, import_react27.useMemo)(
|
|
3817
|
+
() => ({
|
|
3818
|
+
amount,
|
|
3819
|
+
currency,
|
|
3820
|
+
state,
|
|
3821
|
+
clientSecret,
|
|
3822
|
+
stripe: null,
|
|
3823
|
+
elements: null,
|
|
3824
|
+
isReady: false,
|
|
3825
|
+
isProcessing: false,
|
|
3826
|
+
paymentInputComplete: false,
|
|
3827
|
+
canSubmit: false,
|
|
3828
|
+
error: outerError,
|
|
3829
|
+
returnUrl,
|
|
3830
|
+
setPaymentInputComplete: noopSet,
|
|
3831
|
+
submit: noopSubmit
|
|
3832
|
+
}),
|
|
3833
|
+
[amount, currency, state, clientSecret, outerError, returnUrl, noopSet, noopSubmit]
|
|
3834
|
+
);
|
|
3835
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(TopupFormContext.Provider, { value: ctx, children });
|
|
3836
|
+
};
|
|
3837
|
+
var PaymentElementSlot2 = ({ options }) => {
|
|
3838
|
+
const { setPaymentInputComplete, isReady, stripe, elements } = useTopupCtx("PaymentElement");
|
|
3839
|
+
const locale = useLocale();
|
|
3840
|
+
if (!stripe || !elements) return null;
|
|
3841
|
+
if (!isReady) {
|
|
3842
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { "data-solvapay-topup-form-loading": "", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spinner, { size: "sm" }) });
|
|
3843
|
+
}
|
|
3844
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { "data-solvapay-topup-form-payment-element": "", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3845
|
+
import_react_stripe_js2.PaymentElement,
|
|
3846
|
+
{
|
|
3847
|
+
options,
|
|
3848
|
+
onChange: (e) => setPaymentInputComplete(e.complete)
|
|
3849
|
+
},
|
|
3850
|
+
locale || "default"
|
|
3851
|
+
) });
|
|
3852
|
+
};
|
|
3853
|
+
var SubmitButton2 = (0, import_react27.forwardRef)(function TopupFormSubmitButton({ asChild, onClick, children, ...rest }, forwardedRef) {
|
|
3854
|
+
const ctx = useTopupCtx("SubmitButton");
|
|
3855
|
+
const copy = useCopy();
|
|
3856
|
+
const dataState = ctx.isProcessing ? "processing" : !ctx.canSubmit ? "disabled" : "idle";
|
|
3857
|
+
const content = ctx.isProcessing ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
|
|
3858
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spinner, { size: "sm" }),
|
|
3859
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: copy.cta.processing })
|
|
3860
|
+
] }) : children ? children : copy.cta.topUp;
|
|
3861
|
+
const commonProps = {
|
|
3862
|
+
"data-solvapay-topup-form-submit": "",
|
|
3863
|
+
"data-state": dataState,
|
|
3864
|
+
"aria-busy": ctx.isProcessing,
|
|
3865
|
+
"aria-disabled": !ctx.canSubmit,
|
|
3866
|
+
disabled: !ctx.canSubmit,
|
|
3867
|
+
onClick: composeEventHandlers(onClick, (e) => {
|
|
3868
|
+
e.preventDefault();
|
|
3869
|
+
void ctx.submit();
|
|
3870
|
+
}),
|
|
3871
|
+
...rest
|
|
3872
|
+
};
|
|
3873
|
+
if (asChild) {
|
|
3874
|
+
return (
|
|
3875
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3876
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: content })
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { ref: forwardedRef, type: "submit", ...commonProps, children: content });
|
|
3880
|
+
});
|
|
3881
|
+
var Loading4 = (0, import_react27.forwardRef)(function TopupFormLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
3882
|
+
const ctx = useTopupCtx("Loading");
|
|
3883
|
+
if (ctx.state !== "loading") return null;
|
|
3884
|
+
const Comp = asChild ? Slot : "div";
|
|
3885
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Comp, { ref: forwardedRef, "data-solvapay-topup-form-loading": "", ...rest, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Spinner, { size: "sm" }) });
|
|
3886
|
+
});
|
|
3887
|
+
var ErrorSlot4 = (0, import_react27.forwardRef)(function TopupFormError({ asChild, children, ...rest }, forwardedRef) {
|
|
3888
|
+
const ctx = useTopupCtx("Error");
|
|
3889
|
+
if (!ctx.error) return null;
|
|
3890
|
+
const Comp = asChild ? Slot : "div";
|
|
3891
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
|
|
3892
|
+
Comp,
|
|
3893
|
+
{
|
|
3894
|
+
ref: forwardedRef,
|
|
3895
|
+
role: "alert",
|
|
3896
|
+
"aria-live": "assertive",
|
|
3897
|
+
"aria-atomic": "true",
|
|
3898
|
+
"data-solvapay-topup-form-error": "",
|
|
3899
|
+
...rest,
|
|
3900
|
+
children: children ?? ctx.error
|
|
3901
|
+
}
|
|
3902
|
+
);
|
|
3903
|
+
});
|
|
3904
|
+
var TopupFormRoot2 = Root7;
|
|
3905
|
+
var TopupFormPaymentElement = PaymentElementSlot2;
|
|
3906
|
+
var TopupFormSubmitButton2 = SubmitButton2;
|
|
3907
|
+
var TopupFormLoading2 = Loading4;
|
|
3908
|
+
var TopupFormError2 = ErrorSlot4;
|
|
3909
|
+
var TopupForm = {
|
|
3910
|
+
Root: Root7,
|
|
3911
|
+
AmountPicker: AmountPicker.Root,
|
|
3912
|
+
PaymentElement: PaymentElementSlot2,
|
|
3913
|
+
SubmitButton: SubmitButton2,
|
|
3914
|
+
Loading: Loading4,
|
|
3915
|
+
Error: ErrorSlot4
|
|
3916
|
+
};
|
|
3917
|
+
function useTopupForm() {
|
|
3918
|
+
return useTopupCtx("useTopupForm");
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
// src/TopupForm.tsx
|
|
3922
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
3923
|
+
var TopupForm2 = (props) => {
|
|
3924
|
+
const { className, buttonClassName, submitButtonText, ...rootProps } = props;
|
|
3925
|
+
const rootClass = ["solvapay-topup-form", className].filter(Boolean).join(" ");
|
|
3926
|
+
const buttonClass = ["solvapay-topup-form-submit", buttonClassName].filter(Boolean).join(" ");
|
|
3927
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(TopupForm.Root, { ...rootProps, className: rootClass, children: [
|
|
3928
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TopupForm.PaymentElement, {}),
|
|
3929
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TopupForm.Error, { className: "solvapay-topup-form-error" }),
|
|
3930
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TopupForm.Loading, { className: "solvapay-topup-form-loading" }),
|
|
3931
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(TopupForm.SubmitButton, { className: buttonClass, children: submitButtonText })
|
|
3932
|
+
] });
|
|
3933
|
+
};
|
|
3934
|
+
|
|
3935
|
+
// src/primitives/CreditGate.tsx
|
|
3936
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
3937
|
+
var CreditGateContext = (0, import_react28.createContext)(null);
|
|
3938
|
+
function useGateCtx(part) {
|
|
3939
|
+
const ctx = (0, import_react28.useContext)(CreditGateContext);
|
|
3940
|
+
if (!ctx) {
|
|
3941
|
+
throw new Error(`CreditGate.${part} must be rendered inside <CreditGate.Root>.`);
|
|
3942
|
+
}
|
|
3943
|
+
return ctx;
|
|
3944
|
+
}
|
|
3945
|
+
var Root8 = (0, import_react28.forwardRef)(function CreditGateRoot({
|
|
3946
|
+
minCredits = 1,
|
|
3947
|
+
productRef,
|
|
3948
|
+
topupAmount = 1e3,
|
|
3949
|
+
topupCurrency = "usd",
|
|
3950
|
+
asChild,
|
|
3951
|
+
children,
|
|
3952
|
+
...rest
|
|
3953
|
+
}, forwardedRef) {
|
|
3954
|
+
const solva = (0, import_react28.useContext)(SolvaPayContext);
|
|
3955
|
+
if (!solva) throw new MissingProviderError("CreditGate");
|
|
3956
|
+
const { credits, loading } = useBalance();
|
|
3957
|
+
const { product } = useProduct(productRef);
|
|
3958
|
+
const hasCredits = credits != null && credits >= minCredits;
|
|
3959
|
+
const state = loading && credits == null ? "loading" : hasCredits ? "allowed" : "blocked";
|
|
3960
|
+
const ctx = (0, import_react28.useMemo)(
|
|
3961
|
+
() => ({
|
|
3962
|
+
state,
|
|
3963
|
+
loading,
|
|
3964
|
+
hasCredits,
|
|
3965
|
+
balance: credits,
|
|
3966
|
+
productName: product?.name ?? null,
|
|
3967
|
+
topupAmount,
|
|
3968
|
+
topupCurrency
|
|
3969
|
+
}),
|
|
3970
|
+
[state, loading, hasCredits, credits, product?.name, topupAmount, topupCurrency]
|
|
3971
|
+
);
|
|
3972
|
+
const Comp = asChild ? Slot : "div";
|
|
3973
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CreditGateContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
3974
|
+
Comp,
|
|
3975
|
+
{
|
|
3976
|
+
ref: forwardedRef,
|
|
3977
|
+
"data-solvapay-credit-gate": "",
|
|
3978
|
+
"data-state": state,
|
|
3979
|
+
...rest,
|
|
3980
|
+
children
|
|
3981
|
+
}
|
|
3982
|
+
) });
|
|
3983
|
+
});
|
|
3984
|
+
var Heading3 = (0, import_react28.forwardRef)(function CreditGateHeading({ asChild, children, ...rest }, forwardedRef) {
|
|
3985
|
+
const ctx = useGateCtx("Heading");
|
|
3986
|
+
const copy = useCopy();
|
|
3987
|
+
if (ctx.state !== "blocked") return null;
|
|
3988
|
+
const Comp = asChild ? Slot : "h3";
|
|
3989
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-credit-gate-heading": "", ...rest, children: children ?? copy.creditGate.lowBalanceHeading });
|
|
3990
|
+
});
|
|
3991
|
+
var Subheading = (0, import_react28.forwardRef)(function CreditGateSubheading({ asChild, children, ...rest }, forwardedRef) {
|
|
3992
|
+
const ctx = useGateCtx("Subheading");
|
|
3993
|
+
const copy = useCopy();
|
|
3994
|
+
if (ctx.state !== "blocked") return null;
|
|
3995
|
+
const text = interpolate(copy.creditGate.lowBalanceSubheading, {
|
|
3996
|
+
product: ctx.productName ?? "this product"
|
|
3997
|
+
});
|
|
3998
|
+
const Comp = asChild ? Slot : "p";
|
|
3999
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-credit-gate-subheading": "", ...rest, children: children ?? text });
|
|
4000
|
+
});
|
|
4001
|
+
var Topup = ({ amount, currency }) => {
|
|
4002
|
+
const ctx = useGateCtx("Topup");
|
|
4003
|
+
if (ctx.state !== "blocked") return null;
|
|
4004
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
4005
|
+
TopupForm2,
|
|
4006
|
+
{
|
|
4007
|
+
amount: amount ?? ctx.topupAmount,
|
|
4008
|
+
currency: currency ?? ctx.topupCurrency
|
|
4009
|
+
}
|
|
4010
|
+
);
|
|
4011
|
+
};
|
|
4012
|
+
var Loading5 = (0, import_react28.forwardRef)(function CreditGateLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
4013
|
+
const ctx = useGateCtx("Loading");
|
|
4014
|
+
if (ctx.state !== "loading") return null;
|
|
4015
|
+
const Comp = asChild ? Slot : "div";
|
|
4016
|
+
return (
|
|
4017
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4018
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-credit-gate-loading": "", ...rest, children })
|
|
4019
|
+
);
|
|
4020
|
+
});
|
|
4021
|
+
var ErrorSlot5 = (0, import_react28.forwardRef)(function CreditGateError({ asChild, children, ...rest }, forwardedRef) {
|
|
4022
|
+
if (!children) return null;
|
|
4023
|
+
const Comp = asChild ? Slot : "div";
|
|
4024
|
+
return (
|
|
4025
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4026
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Comp, { ref: forwardedRef, "data-solvapay-credit-gate-error": "", role: "alert", ...rest, children })
|
|
4027
|
+
);
|
|
4028
|
+
});
|
|
4029
|
+
var CreditGateRoot2 = Root8;
|
|
4030
|
+
var CreditGateHeading2 = Heading3;
|
|
4031
|
+
var CreditGateSubheading2 = Subheading;
|
|
4032
|
+
var CreditGateTopup = Topup;
|
|
4033
|
+
var CreditGateLoading2 = Loading5;
|
|
4034
|
+
var CreditGateError2 = ErrorSlot5;
|
|
4035
|
+
var CreditGate = {
|
|
4036
|
+
Root: Root8,
|
|
4037
|
+
Heading: Heading3,
|
|
4038
|
+
Subheading,
|
|
4039
|
+
Topup,
|
|
4040
|
+
Loading: Loading5,
|
|
4041
|
+
Error: ErrorSlot5
|
|
4042
|
+
};
|
|
4043
|
+
function useCreditGate() {
|
|
4044
|
+
return useGateCtx("useCreditGate");
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/primitives/PurchaseGate.tsx
|
|
4048
|
+
var import_react29 = require("react");
|
|
4049
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
4050
|
+
var PurchaseGateContext = (0, import_react29.createContext)(null);
|
|
4051
|
+
function useGateCtx2(part) {
|
|
4052
|
+
const ctx = (0, import_react29.useContext)(PurchaseGateContext);
|
|
4053
|
+
if (!ctx) {
|
|
4054
|
+
throw new Error(`PurchaseGate.${part} must be rendered inside <PurchaseGate.Root>.`);
|
|
4055
|
+
}
|
|
4056
|
+
return ctx;
|
|
4057
|
+
}
|
|
4058
|
+
var Root9 = (0, import_react29.forwardRef)(function PurchaseGateRoot({ requirePlan, requireProduct, asChild, children, ...rest }, forwardedRef) {
|
|
4059
|
+
const solva = (0, import_react29.useContext)(SolvaPayContext);
|
|
4060
|
+
if (!solva) throw new MissingProviderError("PurchaseGate");
|
|
4061
|
+
const { purchases, loading, hasProduct, error } = usePurchase();
|
|
4062
|
+
const productToCheck = requireProduct || requirePlan;
|
|
4063
|
+
const hasAccess = productToCheck ? hasProduct(productToCheck) : purchases.some((p) => p.status === "active");
|
|
4064
|
+
const state = loading ? "loading" : hasAccess ? "allowed" : "blocked";
|
|
4065
|
+
const ctx = (0, import_react29.useMemo)(
|
|
4066
|
+
() => ({ state, loading, hasAccess, error }),
|
|
4067
|
+
[state, loading, hasAccess, error]
|
|
4068
|
+
);
|
|
4069
|
+
const Comp = asChild ? Slot : "div";
|
|
4070
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(PurchaseGateContext.Provider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
4071
|
+
Comp,
|
|
4072
|
+
{
|
|
4073
|
+
ref: forwardedRef,
|
|
4074
|
+
"data-solvapay-purchase-gate": "",
|
|
4075
|
+
"data-state": state,
|
|
4076
|
+
...rest,
|
|
4077
|
+
children
|
|
4078
|
+
}
|
|
4079
|
+
) });
|
|
4080
|
+
});
|
|
4081
|
+
var Allowed = (0, import_react29.forwardRef)(function PurchaseGateAllowed({ asChild, children, ...rest }, forwardedRef) {
|
|
4082
|
+
const ctx = useGateCtx2("Allowed");
|
|
4083
|
+
if (ctx.state !== "allowed") return null;
|
|
4084
|
+
const Comp = asChild ? Slot : "div";
|
|
4085
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Comp, { ref: forwardedRef, "data-solvapay-purchase-gate-allowed": "", ...rest, children });
|
|
4086
|
+
});
|
|
4087
|
+
var Blocked = (0, import_react29.forwardRef)(function PurchaseGateBlocked({ asChild, children, ...rest }, forwardedRef) {
|
|
4088
|
+
const ctx = useGateCtx2("Blocked");
|
|
4089
|
+
if (ctx.state !== "blocked") return null;
|
|
4090
|
+
const Comp = asChild ? Slot : "div";
|
|
4091
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Comp, { ref: forwardedRef, "data-solvapay-purchase-gate-blocked": "", ...rest, children });
|
|
4092
|
+
});
|
|
4093
|
+
var Loading6 = (0, import_react29.forwardRef)(function PurchaseGateLoading({ asChild, children, ...rest }, forwardedRef) {
|
|
4094
|
+
const ctx = useGateCtx2("Loading");
|
|
4095
|
+
if (ctx.state !== "loading") return null;
|
|
4096
|
+
const Comp = asChild ? Slot : "div";
|
|
4097
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Comp, { ref: forwardedRef, "data-solvapay-purchase-gate-loading": "", ...rest, children });
|
|
4098
|
+
});
|
|
4099
|
+
var ErrorSlot6 = (0, import_react29.forwardRef)(function PurchaseGateError({ asChild, children, ...rest }, forwardedRef) {
|
|
4100
|
+
const ctx = useGateCtx2("Error");
|
|
4101
|
+
if (!ctx.error) return null;
|
|
4102
|
+
const Comp = asChild ? Slot : "div";
|
|
4103
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Comp, { ref: forwardedRef, "data-solvapay-purchase-gate-error": "", role: "alert", ...rest, children: children ?? ctx.error.message });
|
|
4104
|
+
});
|
|
4105
|
+
var PurchaseGateRoot2 = Root9;
|
|
4106
|
+
var PurchaseGateAllowed2 = Allowed;
|
|
4107
|
+
var PurchaseGateBlocked2 = Blocked;
|
|
4108
|
+
var PurchaseGateLoading2 = Loading6;
|
|
4109
|
+
var PurchaseGateError2 = ErrorSlot6;
|
|
4110
|
+
var PurchaseGate = {
|
|
4111
|
+
Root: Root9,
|
|
4112
|
+
Allowed,
|
|
4113
|
+
Blocked,
|
|
4114
|
+
Loading: Loading6,
|
|
4115
|
+
Error: ErrorSlot6
|
|
4116
|
+
};
|
|
4117
|
+
function usePurchaseGate() {
|
|
4118
|
+
return useGateCtx2("usePurchaseGate");
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// src/primitives/ProductBadge.tsx
|
|
4122
|
+
var import_react30 = require("react");
|
|
4123
|
+
var import_jsx_runtime21 = require("react/jsx-runtime");
|
|
4124
|
+
var ProductBadgeImpl = (0, import_react30.forwardRef)(function ProductBadge({ asChild, children, ...rest }, forwardedRef) {
|
|
4125
|
+
const solva = (0, import_react30.useContext)(SolvaPayContext);
|
|
4126
|
+
if (!solva) throw new MissingProviderError("ProductBadge");
|
|
4127
|
+
const { purchases, loading, hasPaidPurchase, activePurchase } = usePurchase();
|
|
4128
|
+
const copy = useCopy();
|
|
4129
|
+
const [hasLoadedOnce, setHasLoadedOnce] = (0, import_react30.useState)(false);
|
|
4130
|
+
(0, import_react30.useEffect)(() => {
|
|
4131
|
+
if (!loading) setHasLoadedOnce(true);
|
|
4132
|
+
}, [loading]);
|
|
4133
|
+
const planToDisplay = activePurchase?.productName ?? null;
|
|
4134
|
+
const shouldShow = planToDisplay !== null && (!loading || hasLoadedOnce);
|
|
4135
|
+
if (!shouldShow) return null;
|
|
4136
|
+
const commonProps = {
|
|
4137
|
+
"data-solvapay-product-badge": "",
|
|
4138
|
+
"data-loading": loading ? "" : void 0,
|
|
4139
|
+
"data-has-purchase": activePurchase ? "" : void 0,
|
|
4140
|
+
"data-has-paid-purchase": hasPaidPurchase ? "" : void 0,
|
|
4141
|
+
role: "status",
|
|
4142
|
+
"aria-live": "polite",
|
|
4143
|
+
"aria-busy": loading,
|
|
4144
|
+
"aria-label": interpolate(copy.product.currentProductLabel, { name: planToDisplay }),
|
|
4145
|
+
...rest
|
|
4146
|
+
};
|
|
4147
|
+
if (asChild) {
|
|
4148
|
+
return (
|
|
4149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4150
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Slot, { ref: forwardedRef, ...commonProps, children: children ?? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_jsx_runtime21.Fragment, { children: planToDisplay }) })
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
void purchases;
|
|
4154
|
+
return (
|
|
4155
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4156
|
+
/* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { ref: forwardedRef, ...commonProps, children: children ?? planToDisplay })
|
|
4157
|
+
);
|
|
4158
|
+
});
|
|
4159
|
+
var ProductBadge2 = ProductBadgeImpl;
|
|
4160
|
+
var PlanBadge = ProductBadgeImpl;
|
|
4161
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4162
|
+
0 && (module.exports = {
|
|
4163
|
+
ActivationFlow,
|
|
4164
|
+
ActivationFlowActivateButton,
|
|
4165
|
+
ActivationFlowActivated,
|
|
4166
|
+
ActivationFlowAmountPicker,
|
|
4167
|
+
ActivationFlowContinueButton,
|
|
4168
|
+
ActivationFlowError,
|
|
4169
|
+
ActivationFlowLoading,
|
|
4170
|
+
ActivationFlowRetrying,
|
|
4171
|
+
ActivationFlowRoot,
|
|
4172
|
+
ActivationFlowSummary,
|
|
4173
|
+
AmountPicker,
|
|
4174
|
+
AmountPickerConfirm,
|
|
4175
|
+
AmountPickerCustom,
|
|
4176
|
+
AmountPickerOption,
|
|
4177
|
+
AmountPickerRoot,
|
|
4178
|
+
BalanceBadge,
|
|
4179
|
+
CancelPlanButton,
|
|
4180
|
+
CancelledPlanNotice,
|
|
4181
|
+
CancelledPlanNoticeAccessUntil,
|
|
4182
|
+
CancelledPlanNoticeCancelledOn,
|
|
4183
|
+
CancelledPlanNoticeDaysRemaining,
|
|
4184
|
+
CancelledPlanNoticeExpires,
|
|
4185
|
+
CancelledPlanNoticeHeading,
|
|
4186
|
+
CancelledPlanNoticeReactivateButton,
|
|
4187
|
+
CancelledPlanNoticeReason,
|
|
4188
|
+
CancelledPlanNoticeRoot,
|
|
4189
|
+
CheckoutSummary,
|
|
4190
|
+
CheckoutSummaryPlan,
|
|
4191
|
+
CheckoutSummaryPrice,
|
|
4192
|
+
CheckoutSummaryProduct,
|
|
4193
|
+
CheckoutSummaryRoot,
|
|
4194
|
+
CheckoutSummaryTaxNote,
|
|
4195
|
+
CheckoutSummaryTrial,
|
|
4196
|
+
CreditGate,
|
|
4197
|
+
CreditGateError,
|
|
4198
|
+
CreditGateHeading,
|
|
4199
|
+
CreditGateLoading,
|
|
4200
|
+
CreditGateRoot,
|
|
4201
|
+
CreditGateSubheading,
|
|
4202
|
+
CreditGateTopup,
|
|
4203
|
+
MandateText,
|
|
4204
|
+
PaymentForm,
|
|
4205
|
+
PaymentFormCardElement,
|
|
4206
|
+
PaymentFormCustomerFields,
|
|
4207
|
+
PaymentFormError,
|
|
4208
|
+
PaymentFormLoading,
|
|
4209
|
+
PaymentFormMandateText,
|
|
4210
|
+
PaymentFormPaymentElement,
|
|
4211
|
+
PaymentFormRoot,
|
|
4212
|
+
PaymentFormSubmitButton,
|
|
4213
|
+
PaymentFormSummary,
|
|
4214
|
+
PaymentFormTermsCheckbox,
|
|
4215
|
+
PlanBadge,
|
|
4216
|
+
PlanSelector,
|
|
4217
|
+
PlanSelectorCard,
|
|
4218
|
+
PlanSelectorCardBadge,
|
|
4219
|
+
PlanSelectorCardInterval,
|
|
4220
|
+
PlanSelectorCardName,
|
|
4221
|
+
PlanSelectorCardPrice,
|
|
4222
|
+
PlanSelectorError,
|
|
4223
|
+
PlanSelectorGrid,
|
|
4224
|
+
PlanSelectorHeading,
|
|
4225
|
+
PlanSelectorLoading,
|
|
4226
|
+
PlanSelectorRoot,
|
|
4227
|
+
ProductBadge,
|
|
4228
|
+
PurchaseGate,
|
|
4229
|
+
PurchaseGateAllowed,
|
|
4230
|
+
PurchaseGateBlocked,
|
|
4231
|
+
PurchaseGateError,
|
|
4232
|
+
PurchaseGateLoading,
|
|
4233
|
+
PurchaseGateRoot,
|
|
4234
|
+
Slot,
|
|
4235
|
+
Slottable,
|
|
4236
|
+
TopupForm,
|
|
4237
|
+
TopupFormError,
|
|
4238
|
+
TopupFormLoading,
|
|
4239
|
+
TopupFormPaymentElement,
|
|
4240
|
+
TopupFormRoot,
|
|
4241
|
+
TopupFormSubmitButton,
|
|
4242
|
+
composeEventHandlers,
|
|
4243
|
+
composeRefs,
|
|
4244
|
+
setRef,
|
|
4245
|
+
useActivationFlow,
|
|
4246
|
+
useAmountPicker,
|
|
4247
|
+
useAmountPickerCopy,
|
|
4248
|
+
useCancelledPlanNotice,
|
|
4249
|
+
useCheckoutSummary,
|
|
4250
|
+
useCreditGate,
|
|
4251
|
+
usePlanSelector,
|
|
4252
|
+
usePurchaseGate,
|
|
4253
|
+
useTopupForm
|
|
4254
|
+
});
|