@burdenoff/microfe-billing 2026.1027.2 → 2026.1028.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/modules/addons/hooks/index.js +1 -0
- package/dist/billing/modules/addons/hooks/useAddonPurchaseEligibility.js +27 -0
- package/dist/billing/modules/addons/hooks/useAddonPurchaseEligibility.js.map +1 -0
- package/dist/billing/modules/addons/index.js +1 -0
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js +124 -108
- package/dist/billing/modules/addons/pages/AddonBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +172 -151
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js +296 -260
- package/dist/billing/modules/addons/pages/AddonsBrowsePage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +113 -101
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/generated/global-operations.js +335 -311
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,128 +7,140 @@ import { useCallback as i, useEffect as a, useRef as o, useState as s } from "re
|
|
|
7
7
|
import { AlertCircle as c, CheckCircle2 as l, CreditCard as u, Loader2 as d, X as f } from "lucide-react";
|
|
8
8
|
import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
9
9
|
import { useI18n as g } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
10
|
+
import { createPortal as _ } from "react-dom";
|
|
11
|
+
import { ASSISTANT_BOTTOM_CHROME_CLEARANCE as v } from "@burdenoff/fe-libs/shared/assistant";
|
|
10
12
|
//#region src/billing/modules/dashboard/components/AddPaymentMethodModal.tsx
|
|
11
|
-
var
|
|
13
|
+
var y = (e, t) => new Promise((n, r) => {
|
|
12
14
|
if (document.getElementById(t)) {
|
|
13
15
|
n();
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
18
|
let i = document.createElement("script");
|
|
17
19
|
i.id = t, i.src = e, i.async = !0, i.onload = () => n(), i.onerror = () => r(/* @__PURE__ */ Error(`Failed to load ${e}`)), document.head.appendChild(i);
|
|
18
|
-
}),
|
|
19
|
-
let { t:
|
|
20
|
-
let n =
|
|
20
|
+
}), b = ({ isOpen: b, onClose: x, billingAccountId: S, onSuccess: C }) => {
|
|
21
|
+
let { t: w } = g(), T = (e, t) => {
|
|
22
|
+
let n = w(e);
|
|
21
23
|
return n === e ? t : n;
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
+
}, E = o(null), D = o(null), O = o(null), k = o(null), { stripeEnabled: A, stripePublishableKey: j, isLoading: M } = r(!b), { isAddingPaymentMethod: N, verifyPaymentMethod: P } = n(), F = A, [I, L] = s("select-provider"), [R, z] = s(null), [B, V] = s(null), [H, U] = s(!1), [W, G] = s(!1), [K, q] = s(""), [J, Y] = s(!0), [X, Z] = s(!1), Q = i((e) => {
|
|
25
|
+
z(e), L("add-card"), V(null);
|
|
24
26
|
}, []);
|
|
25
27
|
a(() => {
|
|
26
|
-
|
|
27
|
-
}, [
|
|
28
|
-
|
|
28
|
+
b ? (L("select-provider"), z(null), V(null), q(""), Y(!0), Z(!1)) : O.current &&= (O.current.destroy(), null);
|
|
29
|
+
}, [b]), a(() => {
|
|
30
|
+
I !== "select-provider" || M || F && Q("stripe");
|
|
29
31
|
}, [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
I,
|
|
33
|
+
M,
|
|
34
|
+
F,
|
|
35
|
+
Q
|
|
34
36
|
]), a(() => {
|
|
35
|
-
if (!(
|
|
36
|
-
if (!
|
|
37
|
-
|
|
37
|
+
if (!(I !== "add-card" || R !== "stripe")) {
|
|
38
|
+
if (!j && !M) {
|
|
39
|
+
V("Stripe is not configured. Please contact support.");
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
+
return j ? ((async () => {
|
|
43
|
+
G(!0);
|
|
42
44
|
try {
|
|
43
|
-
if (await
|
|
44
|
-
let e = window.Stripe(
|
|
45
|
-
|
|
45
|
+
if (await y("https://js.stripe.com/v3/", "stripe-js"), !window.Stripe) throw Error("Stripe SDK failed to load");
|
|
46
|
+
let e = window.Stripe(j);
|
|
47
|
+
k.current = e;
|
|
46
48
|
let n = 0;
|
|
47
|
-
for (; !
|
|
48
|
-
if (!
|
|
49
|
-
if (
|
|
49
|
+
for (; !D.current && n < 50;) await new Promise((e) => setTimeout(e, 100)), n++;
|
|
50
|
+
if (!D.current) throw Error("Card element container not found after 5 seconds");
|
|
51
|
+
if (O.current) return;
|
|
50
52
|
let r = e.elements().create("card", { style: t("system-ui, -apple-system, sans-serif") });
|
|
51
|
-
r.mount(
|
|
52
|
-
|
|
53
|
+
r.mount(D.current), O.current = r, r.on("change", (e) => {
|
|
54
|
+
Z(e.complete), e.error ? V(e.error.message) : V(null);
|
|
53
55
|
});
|
|
54
56
|
} catch (e) {
|
|
55
|
-
|
|
57
|
+
V(e instanceof Error ? e.message : "Failed to load Stripe");
|
|
56
58
|
} finally {
|
|
57
|
-
|
|
59
|
+
G(!1);
|
|
58
60
|
}
|
|
59
61
|
})(), () => {
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
+
if (O.current) try {
|
|
63
|
+
O.current.destroy(), O.current = null;
|
|
62
64
|
} catch {}
|
|
63
65
|
}) : void 0;
|
|
64
66
|
}
|
|
65
67
|
}, [
|
|
66
|
-
P,
|
|
67
68
|
I,
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
R,
|
|
70
|
+
j,
|
|
71
|
+
M
|
|
70
72
|
]), a(() => {
|
|
71
73
|
let e = (e) => {
|
|
72
|
-
e.key === "Escape" &&
|
|
74
|
+
e.key === "Escape" && b && !H && x();
|
|
73
75
|
};
|
|
74
76
|
return document.addEventListener("keydown", e), () => document.removeEventListener("keydown", e);
|
|
75
77
|
}, [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
b,
|
|
79
|
+
H,
|
|
80
|
+
x
|
|
81
|
+
]), a(() => {
|
|
82
|
+
if (!b) return;
|
|
83
|
+
let e = document.documentElement, t = document.body, n = e.style.overflow, r = t.style.overflow;
|
|
84
|
+
return e.style.overflow = "hidden", t.style.overflow = "hidden", () => {
|
|
85
|
+
e.style.overflow = n, t.style.overflow = r;
|
|
86
|
+
};
|
|
87
|
+
}, [b]);
|
|
88
|
+
let $ = i(async () => {
|
|
89
|
+
if (!k.current || !O.current) {
|
|
90
|
+
V("Stripe not initialized");
|
|
83
91
|
return;
|
|
84
92
|
}
|
|
85
|
-
if (!
|
|
86
|
-
|
|
93
|
+
if (!K.trim()) {
|
|
94
|
+
V("Please enter the cardholder name");
|
|
87
95
|
return;
|
|
88
96
|
}
|
|
89
|
-
|
|
97
|
+
U(!0), V(null);
|
|
90
98
|
try {
|
|
91
|
-
let t = await
|
|
92
|
-
billingAccountId:
|
|
99
|
+
let t = await P({
|
|
100
|
+
billingAccountId: S,
|
|
93
101
|
currency: "USD",
|
|
94
102
|
provider: e.STRIPE,
|
|
95
|
-
setAsDefault:
|
|
103
|
+
setAsDefault: J
|
|
96
104
|
});
|
|
97
105
|
if (!t.clientSecret) throw Error("Failed to initiate card verification");
|
|
98
|
-
let { setupIntent: n, error: r } = await
|
|
99
|
-
card:
|
|
100
|
-
billing_details: { name:
|
|
106
|
+
let { setupIntent: n, error: r } = await k.current.confirmCardSetup(t.clientSecret, { payment_method: {
|
|
107
|
+
card: O.current,
|
|
108
|
+
billing_details: { name: K }
|
|
101
109
|
} });
|
|
102
110
|
if (r) throw Error(r.message);
|
|
103
111
|
if (n?.status !== "succeeded") throw Error("Card verification failed");
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
L("success"), await new Promise((e) => setTimeout(e, 2e3)), C?.(), setTimeout(() => {
|
|
113
|
+
x();
|
|
106
114
|
}, 500);
|
|
107
115
|
} catch (e) {
|
|
108
|
-
|
|
116
|
+
V(e instanceof Error ? e.message : "Failed to add payment method"), U(!1);
|
|
109
117
|
}
|
|
110
118
|
}, [
|
|
111
|
-
W,
|
|
112
119
|
K,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
J,
|
|
121
|
+
S,
|
|
122
|
+
P,
|
|
123
|
+
C,
|
|
124
|
+
x
|
|
125
|
+
]), ee = i(() => {
|
|
126
|
+
R === "stripe" && $();
|
|
127
|
+
}, [R, $]);
|
|
128
|
+
return b ? _(/* @__PURE__ */ h("div", {
|
|
121
129
|
className: "fixed inset-0 z-50 flex items-center justify-center",
|
|
130
|
+
style: {
|
|
131
|
+
paddingTop: "max(1rem, var(--safe-top, 0px))",
|
|
132
|
+
paddingBottom: `max(1rem, ${v})`
|
|
133
|
+
},
|
|
122
134
|
role: "dialog",
|
|
123
135
|
"aria-modal": "true",
|
|
124
136
|
"aria-labelledby": "modal-title",
|
|
125
137
|
children: [/* @__PURE__ */ m("div", {
|
|
126
138
|
className: "absolute inset-0 bg-bg-overlay backdrop-blur-sm",
|
|
127
|
-
onClick:
|
|
139
|
+
onClick: H ? void 0 : x,
|
|
128
140
|
"aria-hidden": "true"
|
|
129
141
|
}), /* @__PURE__ */ h("div", {
|
|
130
|
-
ref:
|
|
131
|
-
className: "relative w-full max-w-md max-h-
|
|
142
|
+
ref: E,
|
|
143
|
+
className: "relative w-full max-w-md max-h-full mx-4 bg-bg-surface border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] flex flex-col",
|
|
132
144
|
children: [
|
|
133
145
|
/* @__PURE__ */ h("header", {
|
|
134
146
|
className: "flex items-center justify-between p-4 border-b border-border-subtle flex-shrink-0",
|
|
@@ -140,20 +152,20 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
140
152
|
}), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h2", {
|
|
141
153
|
id: "modal-title",
|
|
142
154
|
className: "text-lg font-semibold text-text-primary",
|
|
143
|
-
children:
|
|
155
|
+
children: I === "success" ? "Payment Method Added" : "Add Payment Method"
|
|
144
156
|
}), /* @__PURE__ */ h("p", {
|
|
145
157
|
className: "text-sm text-text-secondary",
|
|
146
158
|
children: [
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
I === "select-provider" && (F ? "Add card via Stripe" : "Standalone card save unavailable"),
|
|
160
|
+
I === "add-card" && "Add card via Stripe",
|
|
161
|
+
I === "processing" && "Processing...",
|
|
162
|
+
I === "success" && "Your payment method has been saved"
|
|
151
163
|
]
|
|
152
164
|
})] })]
|
|
153
165
|
}), /* @__PURE__ */ m("button", {
|
|
154
166
|
type: "button",
|
|
155
|
-
onClick:
|
|
156
|
-
disabled:
|
|
167
|
+
onClick: x,
|
|
168
|
+
disabled: H,
|
|
157
169
|
className: "p-2 rounded-button hover:bg-bg-sunken text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50",
|
|
158
170
|
"aria-label": "Close modal",
|
|
159
171
|
children: /* @__PURE__ */ m(f, { className: "size-5" })
|
|
@@ -161,12 +173,12 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
161
173
|
}),
|
|
162
174
|
/* @__PURE__ */ m("div", {
|
|
163
175
|
className: "p-4 overflow-y-auto flex-1",
|
|
164
|
-
children:
|
|
176
|
+
children: M ? /* @__PURE__ */ m("div", {
|
|
165
177
|
className: "flex items-center justify-center py-8",
|
|
166
178
|
children: /* @__PURE__ */ m(d, { className: "size-6 animate-spin text-text-secondary" })
|
|
167
|
-
}) :
|
|
179
|
+
}) : I === "select-provider" ? /* @__PURE__ */ m("div", {
|
|
168
180
|
className: "space-y-3",
|
|
169
|
-
children:
|
|
181
|
+
children: F ? /* @__PURE__ */ m("div", {
|
|
170
182
|
className: "flex items-center justify-center py-8",
|
|
171
183
|
children: /* @__PURE__ */ m(d, { className: "size-6 animate-spin text-text-secondary" })
|
|
172
184
|
}) : /* @__PURE__ */ h("div", {
|
|
@@ -175,7 +187,7 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
175
187
|
/* @__PURE__ */ m(c, { className: "size-12 text-status-warning-text mb-3" }),
|
|
176
188
|
/* @__PURE__ */ m("p", {
|
|
177
189
|
className: "font-medium text-text-primary",
|
|
178
|
-
children:
|
|
190
|
+
children: T("billing.overview.noPaymentProviders", "No Payment Providers Available")
|
|
179
191
|
}),
|
|
180
192
|
/* @__PURE__ */ m("p", {
|
|
181
193
|
className: "text-sm text-text-secondary mt-1",
|
|
@@ -183,17 +195,17 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
183
195
|
})
|
|
184
196
|
]
|
|
185
197
|
})
|
|
186
|
-
}) :
|
|
198
|
+
}) : I === "add-card" ? /* @__PURE__ */ h("div", {
|
|
187
199
|
className: "space-y-4",
|
|
188
200
|
children: [
|
|
189
|
-
|
|
201
|
+
B && /* @__PURE__ */ h("div", {
|
|
190
202
|
className: "flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle border border-status-error-border text-status-error-text",
|
|
191
203
|
children: [/* @__PURE__ */ m(c, { className: "size-4 flex-shrink-0" }), /* @__PURE__ */ m("p", {
|
|
192
204
|
className: "text-sm",
|
|
193
|
-
children:
|
|
205
|
+
children: B
|
|
194
206
|
})]
|
|
195
207
|
}),
|
|
196
|
-
|
|
208
|
+
R === "stripe" && /* @__PURE__ */ h("div", {
|
|
197
209
|
className: "space-y-1.5",
|
|
198
210
|
children: [/* @__PURE__ */ m("label", {
|
|
199
211
|
htmlFor: "cardholderName",
|
|
@@ -202,24 +214,24 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
202
214
|
}), /* @__PURE__ */ m("input", {
|
|
203
215
|
type: "text",
|
|
204
216
|
id: "cardholderName",
|
|
205
|
-
value:
|
|
206
|
-
onChange: (e) =>
|
|
217
|
+
value: K,
|
|
218
|
+
onChange: (e) => q(e.target.value),
|
|
207
219
|
className: "w-full px-3 py-2 border border-border-default rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-border-strong",
|
|
208
220
|
placeholder: "John Doe",
|
|
209
|
-
disabled:
|
|
221
|
+
disabled: H
|
|
210
222
|
})]
|
|
211
223
|
}),
|
|
212
|
-
|
|
224
|
+
R === "stripe" && /* @__PURE__ */ h("div", {
|
|
213
225
|
className: "space-y-1.5",
|
|
214
226
|
children: [/* @__PURE__ */ m("label", {
|
|
215
227
|
className: "block text-sm font-medium text-text-primary",
|
|
216
|
-
children:
|
|
228
|
+
children: T("billing.overview.cardDetails", "Card Details")
|
|
217
229
|
}), /* @__PURE__ */ h("div", {
|
|
218
230
|
className: "relative",
|
|
219
231
|
children: [/* @__PURE__ */ m("div", {
|
|
220
|
-
ref:
|
|
232
|
+
ref: D,
|
|
221
233
|
className: "p-3 border border-border-default rounded-lg bg-bg-surface min-h-[56px]"
|
|
222
|
-
}),
|
|
234
|
+
}), W && /* @__PURE__ */ h("div", {
|
|
223
235
|
className: "absolute inset-0 flex items-center justify-center bg-bg-overlay rounded-lg",
|
|
224
236
|
children: [/* @__PURE__ */ m(d, { className: "size-5 animate-spin text-text-secondary" }), /* @__PURE__ */ m("span", {
|
|
225
237
|
className: "ml-2 text-sm text-text-secondary",
|
|
@@ -232,13 +244,13 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
232
244
|
className: "flex items-center gap-2 cursor-pointer",
|
|
233
245
|
children: [/* @__PURE__ */ m("input", {
|
|
234
246
|
type: "checkbox",
|
|
235
|
-
checked:
|
|
236
|
-
onChange: (e) =>
|
|
247
|
+
checked: J,
|
|
248
|
+
onChange: (e) => Y(e.target.checked),
|
|
237
249
|
className: "size-4 rounded border-border-default text-text-link focus:ring-[var(--color-focus-ring)]",
|
|
238
|
-
disabled:
|
|
250
|
+
disabled: H
|
|
239
251
|
}), /* @__PURE__ */ m("span", {
|
|
240
252
|
className: "text-sm text-text-primary",
|
|
241
|
-
children:
|
|
253
|
+
children: T("billing.overview.setAsDefault", "Set as default payment method")
|
|
242
254
|
})]
|
|
243
255
|
}),
|
|
244
256
|
/* @__PURE__ */ h("div", {
|
|
@@ -261,7 +273,7 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
261
273
|
}),
|
|
262
274
|
/* @__PURE__ */ m("p", {
|
|
263
275
|
className: "font-medium text-text-primary",
|
|
264
|
-
children:
|
|
276
|
+
children: T("billing.overview.paymentMethodAdded", "Payment method added successfully!")
|
|
265
277
|
}),
|
|
266
278
|
/* @__PURE__ */ m("p", {
|
|
267
279
|
className: "text-sm text-text-secondary mt-1",
|
|
@@ -270,30 +282,30 @@ var _ = (e, t) => new Promise((n, r) => {
|
|
|
270
282
|
]
|
|
271
283
|
})
|
|
272
284
|
}),
|
|
273
|
-
|
|
285
|
+
I !== "success" && /* @__PURE__ */ h("footer", {
|
|
274
286
|
className: "flex items-center justify-between gap-3 p-4 border-t border-border-subtle bg-bg-sunken rounded-b-lg flex-shrink-0",
|
|
275
287
|
children: [/* @__PURE__ */ m("div", {}), /* @__PURE__ */ h("div", {
|
|
276
288
|
className: "flex items-center gap-3",
|
|
277
289
|
children: [/* @__PURE__ */ m("button", {
|
|
278
290
|
type: "button",
|
|
279
|
-
onClick:
|
|
280
|
-
disabled:
|
|
291
|
+
onClick: x,
|
|
292
|
+
disabled: H,
|
|
281
293
|
className: "px-4 py-2 text-sm font-medium text-text-primary border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-50",
|
|
282
294
|
children: "Cancel"
|
|
283
|
-
}),
|
|
295
|
+
}), I === "add-card" && /* @__PURE__ */ m("button", {
|
|
284
296
|
type: "button",
|
|
285
|
-
onClick:
|
|
286
|
-
disabled:
|
|
297
|
+
onClick: ee,
|
|
298
|
+
disabled: H || N || W || !X,
|
|
287
299
|
className: "inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-lg hover:bg-action-primary-bgHover transition-colors disabled:opacity-50",
|
|
288
|
-
children:
|
|
300
|
+
children: H || N ? /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m(d, { className: "size-4 animate-spin" }), "Saving..."] }) : W ? /* @__PURE__ */ h(p, { children: [/* @__PURE__ */ m(d, { className: "size-4 animate-spin" }), "Loading…"] }) : "Add Card"
|
|
289
301
|
})]
|
|
290
302
|
})]
|
|
291
303
|
})
|
|
292
304
|
]
|
|
293
305
|
})]
|
|
294
|
-
}) : null;
|
|
306
|
+
}), document.body) : null;
|
|
295
307
|
};
|
|
296
308
|
//#endregion
|
|
297
|
-
export {
|
|
309
|
+
export { b as AddPaymentMethodModal };
|
|
298
310
|
|
|
299
311
|
//# sourceMappingURL=AddPaymentMethodModal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AddPaymentMethodModal.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/components/AddPaymentMethodModal.tsx"],"sourcesContent":["/**\n * Add Payment Method Modal Component\n * Modal for adding a new payment method via Stripe or Razorpay SDKs\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { X, CreditCard, Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePaymentProviderConfig, useDashboardMutations } from '../hooks';\nimport { PaymentProviderPreference } from '../../../shared/types';\nimport { getStripeCardElementStyle } from '../../../shared/utils';\n\n// Stripe types (loaded dynamically)\ndeclare global {\n interface Window {\n Stripe?: (key: string) => StripeInstance;\n }\n}\n\ninterface StripeInstance {\n elements: () => StripeElements;\n createPaymentMethod: (options: {\n type: 'card';\n card: StripeCardElement;\n billing_details?: { name?: string };\n }) => Promise<{\n paymentMethod?: {\n id: string;\n card?: { brand: string; last4: string; exp_month: number; exp_year: number };\n };\n error?: { message: string };\n }>;\n confirmCardSetup: (\n clientSecret: string,\n options?: {\n payment_method?: {\n card: StripeCardElement;\n billing_details?: { name?: string };\n };\n }\n ) => Promise<{\n setupIntent?: {\n id: string;\n status: string;\n payment_method?: string;\n };\n error?: { message: string };\n }>;\n}\n\ninterface StripeElements {\n create: (type: 'card', options?: object) => StripeCardElement;\n}\n\ninterface StripeCardElement {\n mount: (element: HTMLElement | string) => void;\n unmount: () => void;\n on: (\n event: string,\n handler: (event: { complete: boolean; error?: { message: string } }) => void\n ) => void;\n destroy: () => void;\n}\n\ninterface AddPaymentMethodModalProps {\n isOpen: boolean;\n onClose: () => void;\n billingAccountId: string;\n currency: string; // e.g., 'INR', 'USD'\n country?: string; // e.g., 'IN', 'US' - from billing address\n onSuccess?: () => void;\n}\n\ntype ModalStep = 'select-provider' | 'add-card' | 'processing' | 'success';\n\n// Load external script dynamically\nconst loadScript = (src: string, id: string): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (document.getElementById(id)) {\n resolve();\n return;\n }\n const script = document.createElement('script');\n script.id = id;\n script.src = src;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load ${src}`));\n document.head.appendChild(script);\n });\n};\n\nexport const AddPaymentMethodModal: FC<AddPaymentMethodModalProps> = ({\n isOpen,\n onClose,\n billingAccountId,\n onSuccess,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const modalRef = useRef<HTMLDivElement>(null);\n const cardElementRef = useRef<HTMLDivElement>(null);\n const stripeCardElementRef = useRef<StripeCardElement | null>(null);\n const stripeRef = useRef<StripeInstance | null>(null);\n\n // Only fetch payment provider config when modal is open\n const {\n stripeEnabled,\n stripePublishableKey,\n isLoading: isLoadingConfig,\n } = usePaymentProviderConfig(!isOpen);\n const { isAddingPaymentMethod, verifyPaymentMethod } = useDashboardMutations();\n\n // Standalone \"add a card without purchasing anything\" is only offered via\n // Stripe's SetupIntent flow (a genuine $0 card verification). Razorpay has\n // no equivalent zero-charge save: their Checkout.js widget only remembers a\n // card as a side effect of a real payment (customer_id + remember_customer),\n // and our backend's Razorpay \"attach payment method\" call is a documented\n // no-op stub. Offering it here previously meant charging (and refunding) a\n // real ₹1 for a save that didn't actually do anything durable — Razorpay's\n // own remember-card behavior already works during real checkout regardless.\n // Stripe is offered to every account regardless of country/currency, same\n // as checkout itself does (PaymentProviderSelector gates Stripe purely on\n // `stripeEnabled`, never on billing country) — see BOFF-7192 follow-up for\n // a real Razorpay saved-card implementation (Tokens API + Recurring\n // Payments) to close the gap for accounts that only pay via Razorpay.\n const showStripe = stripeEnabled;\n\n const [step, setStep] = useState<ModalStep>('select-provider');\n const [selectedProvider, setSelectedProvider] = useState<'stripe' | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [isProcessing, setIsProcessing] = useState(false);\n const [isLoadingSdk, setIsLoadingSdk] = useState(false);\n const [cardholderName, setCardholderName] = useState('');\n const [setAsDefault, setSetAsDefault] = useState(true);\n const [isCardComplete, setIsCardComplete] = useState(false);\n\n // Must be declared before the effects that reference it\n const handleSelectProvider = useCallback((provider: 'stripe') => {\n setSelectedProvider(provider);\n setStep('add-card');\n setError(null);\n }, []);\n\n // Reset state when modal opens/closes\n useEffect(() => {\n if (isOpen) {\n setStep('select-provider');\n setSelectedProvider(null);\n setError(null);\n setCardholderName('');\n setSetAsDefault(true);\n setIsCardComplete(false);\n } else {\n // Cleanup Stripe element when modal closes\n if (stripeCardElementRef.current) {\n stripeCardElementRef.current.destroy();\n stripeCardElementRef.current = null;\n }\n }\n }, [isOpen]);\n\n // Stripe is the only provider this modal ever offers, so skip straight to\n // the card form when it's available (no provider-selection screen needed).\n useEffect(() => {\n if (step !== 'select-provider' || isLoadingConfig) return;\n if (showStripe) {\n handleSelectProvider('stripe');\n }\n }, [step, isLoadingConfig, showStripe, handleSelectProvider]);\n\n // Initialize Stripe when provider is selected\n useEffect(() => {\n if (step !== 'add-card' || selectedProvider !== 'stripe') return;\n\n // Show error if config loaded but key is missing\n if (!stripePublishableKey && !isLoadingConfig) {\n setError('Stripe is not configured. Please contact support.');\n return;\n }\n\n if (!stripePublishableKey) return;\n\n const initStripe = async () => {\n setIsLoadingSdk(true);\n try {\n await loadScript('https://js.stripe.com/v3/', 'stripe-js');\n\n if (!window.Stripe) {\n throw new Error('Stripe SDK failed to load');\n }\n\n const stripe = window.Stripe(stripePublishableKey);\n stripeRef.current = stripe;\n\n // Wait for the card element container to be mounted in the DOM\n let attempts = 0;\n while (!cardElementRef.current && attempts < 50) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n attempts++;\n }\n\n if (!cardElementRef.current) {\n throw new Error('Card element container not found after 5 seconds');\n }\n\n if (stripeCardElementRef.current) {\n return;\n }\n\n const elements = stripe.elements();\n const cardElement = elements.create('card', {\n style: getStripeCardElementStyle('system-ui, -apple-system, sans-serif'),\n });\n\n cardElement.mount(cardElementRef.current);\n stripeCardElementRef.current = cardElement;\n\n cardElement.on('change', (event) => {\n setIsCardComplete(event.complete);\n if (event.error) {\n setError(event.error.message);\n } else {\n setError(null);\n }\n });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load Stripe');\n } finally {\n setIsLoadingSdk(false);\n }\n };\n\n initStripe();\n\n return () => {\n if (stripeCardElementRef.current) {\n try {\n stripeCardElementRef.current.destroy();\n stripeCardElementRef.current = null;\n } catch {\n // Stripe element may already be detached during modal teardown.\n }\n }\n };\n }, [step, selectedProvider, stripePublishableKey, isLoadingConfig]);\n\n // Handle escape key\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && isOpen && !isProcessing) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [isOpen, isProcessing, onClose]);\n\n const handleStripeSubmit = useCallback(async () => {\n if (!stripeRef.current || !stripeCardElementRef.current) {\n setError('Stripe not initialized');\n return;\n }\n\n if (!cardholderName.trim()) {\n setError('Please enter the cardholder name');\n return;\n }\n\n setIsProcessing(true);\n setError(null);\n\n try {\n // Step 1: Call backend to create SetupIntent and get client secret\n const verificationResult = await verifyPaymentMethod({\n billingAccountId,\n currency: 'USD',\n provider: PaymentProviderPreference.STRIPE,\n setAsDefault,\n });\n\n if (!verificationResult.clientSecret) {\n throw new Error('Failed to initiate card verification');\n }\n\n // Step 2: Confirm card setup using the client secret (no charge)\n const { setupIntent, error: stripeError } = await stripeRef.current.confirmCardSetup(\n verificationResult.clientSecret,\n {\n payment_method: {\n card: stripeCardElementRef.current,\n billing_details: { name: cardholderName },\n },\n }\n );\n\n if (stripeError) {\n throw new Error(stripeError.message);\n }\n\n if (setupIntent?.status !== 'succeeded') {\n throw new Error('Card verification failed');\n }\n\n // Step 3: Show success (webhook will save the payment method)\n setStep('success');\n\n // Wait for webhook to process (2 seconds) before refetching\n await new Promise((resolve) => setTimeout(resolve, 2000));\n onSuccess?.();\n\n // Show success message briefly before closing\n setTimeout(() => {\n onClose();\n }, 500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add payment method');\n setIsProcessing(false);\n }\n }, [cardholderName, setAsDefault, billingAccountId, verifyPaymentMethod, onSuccess, onClose]);\n\n const handleSubmit = useCallback(() => {\n if (selectedProvider === 'stripe') {\n handleStripeSubmit();\n }\n }, [selectedProvider, handleStripeSubmit]);\n\n if (!isOpen) return null;\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"modal-title\"\n >\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-bg-overlay backdrop-blur-sm\"\n onClick={!isProcessing ? onClose : undefined}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div\n ref={modalRef}\n className=\"relative w-full max-w-md max-h-[90vh] mx-4 bg-bg-surface border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] flex flex-col\"\n >\n {/* Header */}\n <header className=\"flex items-center justify-between p-4 border-b border-border-subtle flex-shrink-0\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <CreditCard className=\"size-5 text-text-link\" />\n </div>\n <div>\n <h2 id=\"modal-title\" className=\"text-lg font-semibold text-text-primary\">\n {step === 'success' ? 'Payment Method Added' : 'Add Payment Method'}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {step === 'select-provider' &&\n (showStripe ? 'Add card via Stripe' : 'Standalone card save unavailable')}\n {step === 'add-card' && 'Add card via Stripe'}\n {step === 'processing' && 'Processing...'}\n {step === 'success' && 'Your payment method has been saved'}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isProcessing}\n className=\"p-2 rounded-button hover:bg-bg-sunken text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50\"\n aria-label=\"Close modal\"\n >\n <X className=\"size-5\" />\n </button>\n </header>\n\n {/* Content */}\n <div className=\"p-4 overflow-y-auto flex-1\">\n {isLoadingConfig ? (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n ) : step === 'select-provider' ? (\n <div className=\"space-y-3\">\n {showStripe ? (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n ) : (\n <div className=\"flex flex-col items-center py-8 text-center\">\n <AlertCircle className=\"size-12 text-status-warning-text mb-3\" />\n <p className=\"font-medium text-text-primary\">\n {tr('billing.overview.noPaymentProviders', 'No Payment Providers Available')}\n </p>\n <p className=\"text-sm text-text-secondary mt-1\">\n Card saving isn't configured for this account yet. Please contact support.\n </p>\n </div>\n )}\n </div>\n ) : step === 'add-card' ? (\n <div className=\"space-y-4\">\n {/* Error Message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle border border-status-error-border text-status-error-text\">\n <AlertCircle className=\"size-4 flex-shrink-0\" />\n <p className=\"text-sm\">{error}</p>\n </div>\n )}\n\n {/* Cardholder Name - only for Stripe */}\n {selectedProvider === 'stripe' && (\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"cardholderName\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n Cardholder Name\n </label>\n <input\n type=\"text\"\n id=\"cardholderName\"\n value={cardholderName}\n onChange={(e) => setCardholderName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-default rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-border-strong\"\n placeholder=\"John Doe\"\n disabled={isProcessing}\n />\n </div>\n )}\n\n {/* Stripe Card Element */}\n {selectedProvider === 'stripe' && (\n <div className=\"space-y-1.5\">\n <label className=\"block text-sm font-medium text-text-primary\">\n {tr('billing.overview.cardDetails', 'Card Details')}\n </label>\n <div className=\"relative\">\n <div\n ref={cardElementRef}\n className=\"p-3 border border-border-default rounded-lg bg-bg-surface min-h-[56px]\"\n />\n {isLoadingSdk && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-overlay rounded-lg\">\n <Loader2 className=\"size-5 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-sm text-text-secondary\">Loading Stripe…</span>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Set as default */}\n <label className=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={setAsDefault}\n onChange={(e) => setSetAsDefault(e.target.checked)}\n className=\"size-4 rounded border-border-default text-text-link focus:ring-[var(--color-focus-ring)]\"\n disabled={isProcessing}\n />\n <span className=\"text-sm text-text-primary\">\n {tr('billing.overview.setAsDefault', 'Set as default payment method')}\n </span>\n </label>\n\n {/* Provider badge */}\n <div className=\"flex items-center gap-2 pt-2 border-t border-border-subtle\">\n <span className=\"text-xs text-text-secondary\">Secured by</span>\n <span className=\"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-accent-violet-subtle text-accent-violet\">\n Stripe\n </span>\n </div>\n </div>\n ) : (\n /* Success state */\n <div className=\"flex flex-col items-center py-8 text-center\">\n <div className=\"size-16 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-4\">\n <CheckCircle2 className=\"size-8 text-status-success-text\" />\n </div>\n <p className=\"font-medium text-text-primary\">\n {tr('billing.overview.paymentMethodAdded', 'Payment method added successfully!')}\n </p>\n <p className=\"text-sm text-text-secondary mt-1\">\n Your payment method has been securely saved.\n </p>\n </div>\n )}\n </div>\n\n {/* Footer */}\n {step !== 'success' && (\n <footer className=\"flex items-center justify-between gap-3 p-4 border-t border-border-subtle bg-bg-sunken rounded-b-lg flex-shrink-0\">\n <div />\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isProcessing}\n className=\"px-4 py-2 text-sm font-medium text-text-primary border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n {step === 'add-card' && (\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={\n isProcessing || isAddingPaymentMethod || isLoadingSdk || !isCardComplete\n }\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-lg hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isProcessing || isAddingPaymentMethod ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Saving...\n </>\n ) : isLoadingSdk ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Loading…\n </>\n ) : (\n 'Add Card'\n )}\n </button>\n )}\n </div>\n </footer>\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AddPaymentMethodModal.js","names":[],"sources":["../../../../../src/billing/modules/dashboard/components/AddPaymentMethodModal.tsx"],"sourcesContent":["/**\n * Add Payment Method Modal Component\n * Modal for adding a new payment method via Stripe or Razorpay SDKs\n */\n\nimport { type FC, useState, useCallback, useEffect, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport { X, CreditCard, Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ASSISTANT_BOTTOM_CHROME_CLEARANCE } from '@burdenoff/fe-libs/shared/assistant';\nimport { usePaymentProviderConfig, useDashboardMutations } from '../hooks';\nimport { PaymentProviderPreference } from '../../../shared/types';\nimport { getStripeCardElementStyle } from '../../../shared/utils';\n\n// Stripe types (loaded dynamically)\ndeclare global {\n interface Window {\n Stripe?: (key: string) => StripeInstance;\n }\n}\n\ninterface StripeInstance {\n elements: () => StripeElements;\n createPaymentMethod: (options: {\n type: 'card';\n card: StripeCardElement;\n billing_details?: { name?: string };\n }) => Promise<{\n paymentMethod?: {\n id: string;\n card?: { brand: string; last4: string; exp_month: number; exp_year: number };\n };\n error?: { message: string };\n }>;\n confirmCardSetup: (\n clientSecret: string,\n options?: {\n payment_method?: {\n card: StripeCardElement;\n billing_details?: { name?: string };\n };\n }\n ) => Promise<{\n setupIntent?: {\n id: string;\n status: string;\n payment_method?: string;\n };\n error?: { message: string };\n }>;\n}\n\ninterface StripeElements {\n create: (type: 'card', options?: object) => StripeCardElement;\n}\n\ninterface StripeCardElement {\n mount: (element: HTMLElement | string) => void;\n unmount: () => void;\n on: (\n event: string,\n handler: (event: { complete: boolean; error?: { message: string } }) => void\n ) => void;\n destroy: () => void;\n}\n\ninterface AddPaymentMethodModalProps {\n isOpen: boolean;\n onClose: () => void;\n billingAccountId: string;\n currency: string; // e.g., 'INR', 'USD'\n country?: string; // e.g., 'IN', 'US' - from billing address\n onSuccess?: () => void;\n}\n\ntype ModalStep = 'select-provider' | 'add-card' | 'processing' | 'success';\n\n// Load external script dynamically\nconst loadScript = (src: string, id: string): Promise<void> => {\n return new Promise((resolve, reject) => {\n if (document.getElementById(id)) {\n resolve();\n return;\n }\n const script = document.createElement('script');\n script.id = id;\n script.src = src;\n script.async = true;\n script.onload = () => resolve();\n script.onerror = () => reject(new Error(`Failed to load ${src}`));\n document.head.appendChild(script);\n });\n};\n\nexport const AddPaymentMethodModal: FC<AddPaymentMethodModalProps> = ({\n isOpen,\n onClose,\n billingAccountId,\n onSuccess,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n const modalRef = useRef<HTMLDivElement>(null);\n const cardElementRef = useRef<HTMLDivElement>(null);\n const stripeCardElementRef = useRef<StripeCardElement | null>(null);\n const stripeRef = useRef<StripeInstance | null>(null);\n\n // Only fetch payment provider config when modal is open\n const {\n stripeEnabled,\n stripePublishableKey,\n isLoading: isLoadingConfig,\n } = usePaymentProviderConfig(!isOpen);\n const { isAddingPaymentMethod, verifyPaymentMethod } = useDashboardMutations();\n\n // Standalone \"add a card without purchasing anything\" is only offered via\n // Stripe's SetupIntent flow (a genuine $0 card verification). Razorpay has\n // no equivalent zero-charge save: their Checkout.js widget only remembers a\n // card as a side effect of a real payment (customer_id + remember_customer),\n // and our backend's Razorpay \"attach payment method\" call is a documented\n // no-op stub. Offering it here previously meant charging (and refunding) a\n // real ₹1 for a save that didn't actually do anything durable — Razorpay's\n // own remember-card behavior already works during real checkout regardless.\n // Stripe is offered to every account regardless of country/currency, same\n // as checkout itself does (PaymentProviderSelector gates Stripe purely on\n // `stripeEnabled`, never on billing country) — see BOFF-7192 follow-up for\n // a real Razorpay saved-card implementation (Tokens API + Recurring\n // Payments) to close the gap for accounts that only pay via Razorpay.\n const showStripe = stripeEnabled;\n\n const [step, setStep] = useState<ModalStep>('select-provider');\n const [selectedProvider, setSelectedProvider] = useState<'stripe' | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [isProcessing, setIsProcessing] = useState(false);\n const [isLoadingSdk, setIsLoadingSdk] = useState(false);\n const [cardholderName, setCardholderName] = useState('');\n const [setAsDefault, setSetAsDefault] = useState(true);\n const [isCardComplete, setIsCardComplete] = useState(false);\n\n // Must be declared before the effects that reference it\n const handleSelectProvider = useCallback((provider: 'stripe') => {\n setSelectedProvider(provider);\n setStep('add-card');\n setError(null);\n }, []);\n\n // Reset state when modal opens/closes\n useEffect(() => {\n if (isOpen) {\n setStep('select-provider');\n setSelectedProvider(null);\n setError(null);\n setCardholderName('');\n setSetAsDefault(true);\n setIsCardComplete(false);\n } else {\n // Cleanup Stripe element when modal closes\n if (stripeCardElementRef.current) {\n stripeCardElementRef.current.destroy();\n stripeCardElementRef.current = null;\n }\n }\n }, [isOpen]);\n\n // Stripe is the only provider this modal ever offers, so skip straight to\n // the card form when it's available (no provider-selection screen needed).\n useEffect(() => {\n if (step !== 'select-provider' || isLoadingConfig) return;\n if (showStripe) {\n handleSelectProvider('stripe');\n }\n }, [step, isLoadingConfig, showStripe, handleSelectProvider]);\n\n // Initialize Stripe when provider is selected\n useEffect(() => {\n if (step !== 'add-card' || selectedProvider !== 'stripe') return;\n\n // Show error if config loaded but key is missing\n if (!stripePublishableKey && !isLoadingConfig) {\n setError('Stripe is not configured. Please contact support.');\n return;\n }\n\n if (!stripePublishableKey) return;\n\n const initStripe = async () => {\n setIsLoadingSdk(true);\n try {\n await loadScript('https://js.stripe.com/v3/', 'stripe-js');\n\n if (!window.Stripe) {\n throw new Error('Stripe SDK failed to load');\n }\n\n const stripe = window.Stripe(stripePublishableKey);\n stripeRef.current = stripe;\n\n // Wait for the card element container to be mounted in the DOM\n let attempts = 0;\n while (!cardElementRef.current && attempts < 50) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n attempts++;\n }\n\n if (!cardElementRef.current) {\n throw new Error('Card element container not found after 5 seconds');\n }\n\n if (stripeCardElementRef.current) {\n return;\n }\n\n const elements = stripe.elements();\n const cardElement = elements.create('card', {\n style: getStripeCardElementStyle('system-ui, -apple-system, sans-serif'),\n });\n\n cardElement.mount(cardElementRef.current);\n stripeCardElementRef.current = cardElement;\n\n cardElement.on('change', (event) => {\n setIsCardComplete(event.complete);\n if (event.error) {\n setError(event.error.message);\n } else {\n setError(null);\n }\n });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load Stripe');\n } finally {\n setIsLoadingSdk(false);\n }\n };\n\n initStripe();\n\n return () => {\n if (stripeCardElementRef.current) {\n try {\n stripeCardElementRef.current.destroy();\n stripeCardElementRef.current = null;\n } catch {\n // Stripe element may already be detached during modal teardown.\n }\n }\n };\n }, [step, selectedProvider, stripePublishableKey, isLoadingConfig]);\n\n // Handle escape key\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape' && isOpen && !isProcessing) {\n onClose();\n }\n };\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [isOpen, isProcessing, onClose]);\n\n // Lock background scroll while open. Without this, swiping up on the page\n // behind the modal scrolls it (and, on the installed app, carries the\n // modal along too — see the portal below), so the form content moves out\n // of view. Locking both html and body covers iOS Safari/WebView, which\n // can rubber-band-scroll one but not the other depending on which has the\n // overflow.\n useEffect(() => {\n if (!isOpen) return;\n const htmlEl = document.documentElement;\n const bodyEl = document.body;\n const prevHtmlOverflow = htmlEl.style.overflow;\n const prevBodyOverflow = bodyEl.style.overflow;\n htmlEl.style.overflow = 'hidden';\n bodyEl.style.overflow = 'hidden';\n return () => {\n htmlEl.style.overflow = prevHtmlOverflow;\n bodyEl.style.overflow = prevBodyOverflow;\n };\n }, [isOpen]);\n\n const handleStripeSubmit = useCallback(async () => {\n if (!stripeRef.current || !stripeCardElementRef.current) {\n setError('Stripe not initialized');\n return;\n }\n\n if (!cardholderName.trim()) {\n setError('Please enter the cardholder name');\n return;\n }\n\n setIsProcessing(true);\n setError(null);\n\n try {\n // Step 1: Call backend to create SetupIntent and get client secret\n const verificationResult = await verifyPaymentMethod({\n billingAccountId,\n currency: 'USD',\n provider: PaymentProviderPreference.STRIPE,\n setAsDefault,\n });\n\n if (!verificationResult.clientSecret) {\n throw new Error('Failed to initiate card verification');\n }\n\n // Step 2: Confirm card setup using the client secret (no charge)\n const { setupIntent, error: stripeError } = await stripeRef.current.confirmCardSetup(\n verificationResult.clientSecret,\n {\n payment_method: {\n card: stripeCardElementRef.current,\n billing_details: { name: cardholderName },\n },\n }\n );\n\n if (stripeError) {\n throw new Error(stripeError.message);\n }\n\n if (setupIntent?.status !== 'succeeded') {\n throw new Error('Card verification failed');\n }\n\n // Step 3: Show success (webhook will save the payment method)\n setStep('success');\n\n // Wait for webhook to process (2 seconds) before refetching\n await new Promise((resolve) => setTimeout(resolve, 2000));\n onSuccess?.();\n\n // Show success message briefly before closing\n setTimeout(() => {\n onClose();\n }, 500);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add payment method');\n setIsProcessing(false);\n }\n }, [cardholderName, setAsDefault, billingAccountId, verifyPaymentMethod, onSuccess, onClose]);\n\n const handleSubmit = useCallback(() => {\n if (selectedProvider === 'stripe') {\n handleStripeSubmit();\n }\n }, [selectedProvider, handleStripeSubmit]);\n\n if (!isOpen) return null;\n\n // Portaled to <body>, not rendered inside the page tree: on the installed\n // app, the route wrapper (NativePageTransition) animates with a CSS\n // transform, which makes a `position: fixed` descendant fixed to THAT\n // transformed ancestor instead of the viewport — the modal scrolled with\n // the underlying page instead of staying put. Same root cause and fix as\n // the floating checkout chip (BOFF-9983/9984).\n //\n // The outer wrapper's padding keeps the dialog fully inside the safe\n // area and above the tab bar instead of only centering within 100vh.\n return createPortal(\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center\"\n style={{\n paddingTop: 'max(1rem, var(--safe-top, 0px))',\n paddingBottom: `max(1rem, ${ASSISTANT_BOTTOM_CHROME_CLEARANCE})`,\n }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"modal-title\"\n >\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-bg-overlay backdrop-blur-sm\"\n onClick={!isProcessing ? onClose : undefined}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div\n ref={modalRef}\n className=\"relative w-full max-w-md max-h-full mx-4 bg-bg-surface border border-border-seam rounded-card shadow-[var(--shadow-elevation-4)] flex flex-col\"\n >\n {/* Header */}\n <header className=\"flex items-center justify-between p-4 border-b border-border-subtle flex-shrink-0\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <CreditCard className=\"size-5 text-text-link\" />\n </div>\n <div>\n <h2 id=\"modal-title\" className=\"text-lg font-semibold text-text-primary\">\n {step === 'success' ? 'Payment Method Added' : 'Add Payment Method'}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {step === 'select-provider' &&\n (showStripe ? 'Add card via Stripe' : 'Standalone card save unavailable')}\n {step === 'add-card' && 'Add card via Stripe'}\n {step === 'processing' && 'Processing...'}\n {step === 'success' && 'Your payment method has been saved'}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isProcessing}\n className=\"p-2 rounded-button hover:bg-bg-sunken text-text-secondary hover:text-text-primary transition-colors disabled:opacity-50\"\n aria-label=\"Close modal\"\n >\n <X className=\"size-5\" />\n </button>\n </header>\n\n {/* Content */}\n <div className=\"p-4 overflow-y-auto flex-1\">\n {isLoadingConfig ? (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n ) : step === 'select-provider' ? (\n <div className=\"space-y-3\">\n {showStripe ? (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"size-6 animate-spin text-text-secondary\" />\n </div>\n ) : (\n <div className=\"flex flex-col items-center py-8 text-center\">\n <AlertCircle className=\"size-12 text-status-warning-text mb-3\" />\n <p className=\"font-medium text-text-primary\">\n {tr('billing.overview.noPaymentProviders', 'No Payment Providers Available')}\n </p>\n <p className=\"text-sm text-text-secondary mt-1\">\n Card saving isn't configured for this account yet. Please contact support.\n </p>\n </div>\n )}\n </div>\n ) : step === 'add-card' ? (\n <div className=\"space-y-4\">\n {/* Error Message */}\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-lg bg-status-error-bg-subtle border border-status-error-border text-status-error-text\">\n <AlertCircle className=\"size-4 flex-shrink-0\" />\n <p className=\"text-sm\">{error}</p>\n </div>\n )}\n\n {/* Cardholder Name - only for Stripe */}\n {selectedProvider === 'stripe' && (\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"cardholderName\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n Cardholder Name\n </label>\n <input\n type=\"text\"\n id=\"cardholderName\"\n value={cardholderName}\n onChange={(e) => setCardholderName(e.target.value)}\n className=\"w-full px-3 py-2 border border-border-default rounded-input bg-bg-surface text-text-primary placeholder:text-text-placeholder focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] focus:border-border-strong\"\n placeholder=\"John Doe\"\n disabled={isProcessing}\n />\n </div>\n )}\n\n {/* Stripe Card Element */}\n {selectedProvider === 'stripe' && (\n <div className=\"space-y-1.5\">\n <label className=\"block text-sm font-medium text-text-primary\">\n {tr('billing.overview.cardDetails', 'Card Details')}\n </label>\n <div className=\"relative\">\n <div\n ref={cardElementRef}\n className=\"p-3 border border-border-default rounded-lg bg-bg-surface min-h-[56px]\"\n />\n {isLoadingSdk && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-bg-overlay rounded-lg\">\n <Loader2 className=\"size-5 animate-spin text-text-secondary\" />\n <span className=\"ml-2 text-sm text-text-secondary\">Loading Stripe…</span>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Set as default */}\n <label className=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n checked={setAsDefault}\n onChange={(e) => setSetAsDefault(e.target.checked)}\n className=\"size-4 rounded border-border-default text-text-link focus:ring-[var(--color-focus-ring)]\"\n disabled={isProcessing}\n />\n <span className=\"text-sm text-text-primary\">\n {tr('billing.overview.setAsDefault', 'Set as default payment method')}\n </span>\n </label>\n\n {/* Provider badge */}\n <div className=\"flex items-center gap-2 pt-2 border-t border-border-subtle\">\n <span className=\"text-xs text-text-secondary\">Secured by</span>\n <span className=\"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-accent-violet-subtle text-accent-violet\">\n Stripe\n </span>\n </div>\n </div>\n ) : (\n /* Success state */\n <div className=\"flex flex-col items-center py-8 text-center\">\n <div className=\"size-16 rounded-full bg-status-success-bg-subtle flex items-center justify-center mb-4\">\n <CheckCircle2 className=\"size-8 text-status-success-text\" />\n </div>\n <p className=\"font-medium text-text-primary\">\n {tr('billing.overview.paymentMethodAdded', 'Payment method added successfully!')}\n </p>\n <p className=\"text-sm text-text-secondary mt-1\">\n Your payment method has been securely saved.\n </p>\n </div>\n )}\n </div>\n\n {/* Footer */}\n {step !== 'success' && (\n <footer className=\"flex items-center justify-between gap-3 p-4 border-t border-border-subtle bg-bg-sunken rounded-b-lg flex-shrink-0\">\n <div />\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n disabled={isProcessing}\n className=\"px-4 py-2 text-sm font-medium text-text-primary border border-border-subtle rounded-lg hover:bg-bg-sunken transition-colors disabled:opacity-50\"\n >\n Cancel\n </button>\n {step === 'add-card' && (\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={\n isProcessing || isAddingPaymentMethod || isLoadingSdk || !isCardComplete\n }\n className=\"inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg rounded-lg hover:bg-action-primary-bgHover transition-colors disabled:opacity-50\"\n >\n {isProcessing || isAddingPaymentMethod ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Saving...\n </>\n ) : isLoadingSdk ? (\n <>\n <Loader2 className=\"size-4 animate-spin\" />\n Loading…\n </>\n ) : (\n 'Add Card'\n )}\n </button>\n )}\n </div>\n </footer>\n )}\n </div>\n </div>,\n document.body\n );\n};\n"],"mappings":"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"}
|