@b3dotfun/sdk 0.0.26-alpha.1 → 0.0.26-alpha.2
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.
|
@@ -48,36 +48,22 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
48
48
|
const [amount, setAmount] = (0, react_3.useState)(null);
|
|
49
49
|
const [stripeReady, setStripeReady] = (0, react_3.useState)(false);
|
|
50
50
|
const [showHowItWorks, setShowHowItWorks] = (0, react_3.useState)(false);
|
|
51
|
-
|
|
52
|
-
const [cardComplete, setCardComplete] = (0, react_3.useState)(false);
|
|
53
|
-
const [addressComplete, setAddressComplete] = (0, react_3.useState)(false);
|
|
54
|
-
// Snapshot of AddressElement value to pass to billing_details
|
|
55
|
-
const [addressValue, setAddressValue] = (0, react_3.useState)(null);
|
|
56
|
-
// Helper function to mark "How it works" as seen
|
|
57
|
-
const markHowItWorksAsSeen = (0, react_3.useCallback)(() => {
|
|
58
|
-
setShowHowItWorks(false);
|
|
59
|
-
localStorage.setItem("b3-payment-how-it-works-seen", "true");
|
|
60
|
-
}, []);
|
|
51
|
+
const [showAddressElement, setShowAddressElement] = (0, react_3.useState)(false);
|
|
61
52
|
(0, react_3.useEffect)(() => {
|
|
62
53
|
if (stripe && elements) {
|
|
63
54
|
setStripeReady(true);
|
|
64
55
|
}
|
|
65
56
|
}, [stripe, elements, order.id]);
|
|
66
|
-
// Check if user has seen "How it works" before
|
|
67
|
-
(0, react_3.useEffect)(() => {
|
|
68
|
-
const hasSeenHowItWorks = localStorage.getItem("b3-payment-how-it-works-seen");
|
|
69
|
-
if (!hasSeenHowItWorks) {
|
|
70
|
-
setShowHowItWorks(true);
|
|
71
|
-
}
|
|
72
|
-
}, []);
|
|
73
57
|
(0, react_3.useEffect)(() => {
|
|
74
58
|
const fetchPaymentIntent = async () => {
|
|
75
59
|
if (!stripe || !clientSecret)
|
|
76
60
|
return;
|
|
77
61
|
try {
|
|
78
62
|
const paymentIntent = await stripe.retrievePaymentIntent(clientSecret);
|
|
79
|
-
const
|
|
80
|
-
|
|
63
|
+
const amount = paymentIntent.paymentIntent?.amount
|
|
64
|
+
? (0, payment_utils_1.formatStripeAmount)(paymentIntent.paymentIntent.amount)
|
|
65
|
+
: null;
|
|
66
|
+
setAmount(amount);
|
|
81
67
|
}
|
|
82
68
|
catch (error) {
|
|
83
69
|
console.error("@@stripe-web2-payment:retrieve-intent-error:", JSON.stringify(error, null, 2));
|
|
@@ -85,50 +71,23 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
85
71
|
};
|
|
86
72
|
fetchPaymentIntent();
|
|
87
73
|
}, [clientSecret, stripe]);
|
|
74
|
+
// Handle payment element changes
|
|
75
|
+
const handlePaymentElementChange = (event) => {
|
|
76
|
+
// Show address element only for card payments
|
|
77
|
+
setShowAddressElement(event.value.type === "card");
|
|
78
|
+
};
|
|
88
79
|
const handleSubmit = async (e) => {
|
|
89
80
|
e.preventDefault();
|
|
90
|
-
if (!stripe || !elements
|
|
81
|
+
if (!stripe || !elements) {
|
|
91
82
|
setMessage("Stripe is not initialized");
|
|
92
83
|
return;
|
|
93
84
|
}
|
|
94
85
|
setLoading(true);
|
|
95
86
|
setMessage(null);
|
|
96
|
-
// Block submission until both card and billing address are complete
|
|
97
|
-
if (!cardComplete || !addressComplete) {
|
|
98
|
-
setMessage("Please complete all required billing address and card fields.");
|
|
99
|
-
setLoading(false);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
87
|
try {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
setLoading(false);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const result = (await stripe.confirmCardPayment(clientSecret, {
|
|
110
|
-
payment_method: {
|
|
111
|
-
card,
|
|
112
|
-
billing_details: {
|
|
113
|
-
// Map AddressElement values into billing_details
|
|
114
|
-
name: addressValue?.name
|
|
115
|
-
? typeof addressValue.name === "string"
|
|
116
|
-
? addressValue.name
|
|
117
|
-
: [addressValue.name.firstName, addressValue.name.lastName].filter(Boolean).join(" ")
|
|
118
|
-
: undefined,
|
|
119
|
-
phone: addressValue?.phone || undefined,
|
|
120
|
-
address: addressValue?.address
|
|
121
|
-
? {
|
|
122
|
-
line1: addressValue.address.line1 || undefined,
|
|
123
|
-
line2: addressValue.address.line2 || undefined,
|
|
124
|
-
city: addressValue.address.city || undefined,
|
|
125
|
-
state: addressValue.address.state || undefined, // province/region
|
|
126
|
-
postal_code: addressValue.address.postal_code || undefined,
|
|
127
|
-
country: addressValue.address.country || undefined,
|
|
128
|
-
}
|
|
129
|
-
: undefined,
|
|
130
|
-
},
|
|
131
|
-
},
|
|
88
|
+
const result = (await stripe.confirmPayment({
|
|
89
|
+
elements,
|
|
90
|
+
redirect: "if_required",
|
|
132
91
|
}));
|
|
133
92
|
if (result.error) {
|
|
134
93
|
// This point will only be reached if there is an immediate error.
|
|
@@ -175,6 +134,16 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
175
134
|
if (!stripeReady) {
|
|
176
135
|
return (0, jsx_runtime_1.jsx)(StripeLoadingState, {});
|
|
177
136
|
}
|
|
137
|
+
const stripeElementOptions = {
|
|
138
|
+
layout: "tabs",
|
|
139
|
+
fields: {
|
|
140
|
+
billingDetails: "auto",
|
|
141
|
+
},
|
|
142
|
+
wallets: {
|
|
143
|
+
applePay: "auto",
|
|
144
|
+
googlePay: "auto",
|
|
145
|
+
},
|
|
146
|
+
};
|
|
178
147
|
const howItWorksSteps = [
|
|
179
148
|
{
|
|
180
149
|
number: 1,
|
|
@@ -189,19 +158,20 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
189
158
|
description: "After payment confirmation, your order will be processed and completed automatically",
|
|
190
159
|
},
|
|
191
160
|
];
|
|
192
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative mt-1 flex w-full flex-1 flex-col items-center justify-center", children: [(0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "w-full space-y-6", children: [(0, jsx_runtime_1.jsx)(react_1.OrderDetailsCollapsible, { order: order, dstToken: order.metadata.dstToken, tournament: order.type === "join_tournament" || order.type === "fund_tournament" ? order.metadata.tournament : undefined, nft: order.type === "mint_nft" ? order.metadata.nft : undefined, recipientName: recipientName, formattedExpectedDstAmount: (0, number_1.formatTokenAmount)(BigInt(order.srcAmount), order.metadata.dstToken.decimals), showTotal: true, totalAmount: amount ? `$${Number(amount).toFixed(2)}` : undefined }), (0, jsx_runtime_1.jsxs)("div", { className: "w-full", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary mb-4 text-lg font-semibold", children: "Payment Details" }), (0, jsx_runtime_1.jsx)(
|
|
161
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "relative mt-1 flex w-full flex-1 flex-col items-center justify-center", children: [(0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "w-full space-y-6", children: [(0, jsx_runtime_1.jsx)(react_1.OrderDetailsCollapsible, { order: order, dstToken: order.metadata.dstToken, tournament: order.type === "join_tournament" || order.type === "fund_tournament" ? order.metadata.tournament : undefined, nft: order.type === "mint_nft" ? order.metadata.nft : undefined, recipientName: recipientName, formattedExpectedDstAmount: (0, number_1.formatTokenAmount)(BigInt(order.srcAmount), order.metadata.dstToken.decimals), showTotal: true, totalAmount: amount ? `$${Number(amount).toFixed(2)}` : undefined }), (0, jsx_runtime_1.jsxs)("div", { className: "w-full", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary mb-4 text-lg font-semibold", children: "Payment Details" }), (0, jsx_runtime_1.jsx)(react_stripe_js_1.PaymentElement, { onChange: handlePaymentElementChange, options: stripeElementOptions }), showAddressElement && ((0, jsx_runtime_1.jsx)(react_stripe_js_1.AddressElement, { options: {
|
|
193
162
|
mode: "billing",
|
|
194
|
-
fields: {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
163
|
+
fields: {
|
|
164
|
+
phone: "always",
|
|
165
|
+
},
|
|
166
|
+
// More granular control
|
|
167
|
+
display: {
|
|
168
|
+
name: "split", // or 'split' for first/last name separately
|
|
169
|
+
},
|
|
170
|
+
// Validation
|
|
171
|
+
validation: {
|
|
172
|
+
phone: {
|
|
173
|
+
required: "always", // or 'always', 'never'
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
} }))] }), message && ((0, jsx_runtime_1.jsxs)("div", { className: "bg-as-red/10 border-as-red/20 flex w-full items-center gap-3 rounded-2xl border p-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "bg-as-red flex h-6 w-6 shrink-0 items-center justify-center rounded-full", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-4 w-4 text-white", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-as-red text-sm font-medium", children: message })] })), (0, jsx_runtime_1.jsx)(react_2.ShinyButton, { type: "submit", accentColor: "hsl(var(--as-brand))", disabled: !stripe || !elements || loading, className: "relative w-full py-4 text-lg font-semibold", children: loading ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-current border-t-transparent" }), (0, jsx_runtime_1.jsx)("span", { className: "text-white", children: "Processing Payment..." })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-white", children: "Complete Payment" }), amount && (0, jsx_runtime_1.jsxs)("span", { className: "text-white/90", children: ["$", Number(amount).toFixed(2)] })] })) })] }), showHowItWorks && ((0, jsx_runtime_1.jsx)("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-as-on-surface-1 relative max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-2xl p-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-6 flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-as-primary text-xl font-semibold", children: "How it works" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setShowHowItWorks(false), className: "text-as-primary/60 hover:text-as-primary transition-colors", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "h-6 w-6" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-6", children: [(0, jsx_runtime_1.jsx)(PaymentMethodIcons_1.default, {}), (0, jsx_runtime_1.jsx)(HowItWorks_1.default, { steps: howItWorksSteps })] })] }) }))] }));
|
|
207
177
|
}
|
|
@@ -4,10 +4,10 @@ import { OrderDetailsCollapsible, useStripeClientSecret } from "../../../../anys
|
|
|
4
4
|
import { ShinyButton, useB3, useModalStore, useProfile } from "../../../../global-account/react/index.js";
|
|
5
5
|
import { formatTokenAmount } from "../../../../shared/utils/number.js";
|
|
6
6
|
import { formatStripeAmount } from "../../../../shared/utils/payment.utils.js";
|
|
7
|
-
import { AddressElement,
|
|
7
|
+
import { AddressElement, Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
|
8
8
|
import { loadStripe } from "@stripe/stripe-js";
|
|
9
9
|
import { X } from "lucide-react";
|
|
10
|
-
import {
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
11
|
import { AnySpendFingerprintWrapper, getFingerprintConfig } from "../AnySpendFingerprintWrapper.js";
|
|
12
12
|
import HowItWorks from "./HowItWorks.js";
|
|
13
13
|
import PaymentMethodIcons from "./PaymentMethodIcons.js";
|
|
@@ -42,36 +42,22 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
42
42
|
const [amount, setAmount] = useState(null);
|
|
43
43
|
const [stripeReady, setStripeReady] = useState(false);
|
|
44
44
|
const [showHowItWorks, setShowHowItWorks] = useState(false);
|
|
45
|
-
|
|
46
|
-
const [cardComplete, setCardComplete] = useState(false);
|
|
47
|
-
const [addressComplete, setAddressComplete] = useState(false);
|
|
48
|
-
// Snapshot of AddressElement value to pass to billing_details
|
|
49
|
-
const [addressValue, setAddressValue] = useState(null);
|
|
50
|
-
// Helper function to mark "How it works" as seen
|
|
51
|
-
const markHowItWorksAsSeen = useCallback(() => {
|
|
52
|
-
setShowHowItWorks(false);
|
|
53
|
-
localStorage.setItem("b3-payment-how-it-works-seen", "true");
|
|
54
|
-
}, []);
|
|
45
|
+
const [showAddressElement, setShowAddressElement] = useState(false);
|
|
55
46
|
useEffect(() => {
|
|
56
47
|
if (stripe && elements) {
|
|
57
48
|
setStripeReady(true);
|
|
58
49
|
}
|
|
59
50
|
}, [stripe, elements, order.id]);
|
|
60
|
-
// Check if user has seen "How it works" before
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
const hasSeenHowItWorks = localStorage.getItem("b3-payment-how-it-works-seen");
|
|
63
|
-
if (!hasSeenHowItWorks) {
|
|
64
|
-
setShowHowItWorks(true);
|
|
65
|
-
}
|
|
66
|
-
}, []);
|
|
67
51
|
useEffect(() => {
|
|
68
52
|
const fetchPaymentIntent = async () => {
|
|
69
53
|
if (!stripe || !clientSecret)
|
|
70
54
|
return;
|
|
71
55
|
try {
|
|
72
56
|
const paymentIntent = await stripe.retrievePaymentIntent(clientSecret);
|
|
73
|
-
const
|
|
74
|
-
|
|
57
|
+
const amount = paymentIntent.paymentIntent?.amount
|
|
58
|
+
? formatStripeAmount(paymentIntent.paymentIntent.amount)
|
|
59
|
+
: null;
|
|
60
|
+
setAmount(amount);
|
|
75
61
|
}
|
|
76
62
|
catch (error) {
|
|
77
63
|
console.error("@@stripe-web2-payment:retrieve-intent-error:", JSON.stringify(error, null, 2));
|
|
@@ -79,50 +65,23 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
79
65
|
};
|
|
80
66
|
fetchPaymentIntent();
|
|
81
67
|
}, [clientSecret, stripe]);
|
|
68
|
+
// Handle payment element changes
|
|
69
|
+
const handlePaymentElementChange = (event) => {
|
|
70
|
+
// Show address element only for card payments
|
|
71
|
+
setShowAddressElement(event.value.type === "card");
|
|
72
|
+
};
|
|
82
73
|
const handleSubmit = async (e) => {
|
|
83
74
|
e.preventDefault();
|
|
84
|
-
if (!stripe || !elements
|
|
75
|
+
if (!stripe || !elements) {
|
|
85
76
|
setMessage("Stripe is not initialized");
|
|
86
77
|
return;
|
|
87
78
|
}
|
|
88
79
|
setLoading(true);
|
|
89
80
|
setMessage(null);
|
|
90
|
-
// Block submission until both card and billing address are complete
|
|
91
|
-
if (!cardComplete || !addressComplete) {
|
|
92
|
-
setMessage("Please complete all required billing address and card fields.");
|
|
93
|
-
setLoading(false);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
81
|
try {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
setLoading(false);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const result = (await stripe.confirmCardPayment(clientSecret, {
|
|
104
|
-
payment_method: {
|
|
105
|
-
card,
|
|
106
|
-
billing_details: {
|
|
107
|
-
// Map AddressElement values into billing_details
|
|
108
|
-
name: addressValue?.name
|
|
109
|
-
? typeof addressValue.name === "string"
|
|
110
|
-
? addressValue.name
|
|
111
|
-
: [addressValue.name.firstName, addressValue.name.lastName].filter(Boolean).join(" ")
|
|
112
|
-
: undefined,
|
|
113
|
-
phone: addressValue?.phone || undefined,
|
|
114
|
-
address: addressValue?.address
|
|
115
|
-
? {
|
|
116
|
-
line1: addressValue.address.line1 || undefined,
|
|
117
|
-
line2: addressValue.address.line2 || undefined,
|
|
118
|
-
city: addressValue.address.city || undefined,
|
|
119
|
-
state: addressValue.address.state || undefined, // province/region
|
|
120
|
-
postal_code: addressValue.address.postal_code || undefined,
|
|
121
|
-
country: addressValue.address.country || undefined,
|
|
122
|
-
}
|
|
123
|
-
: undefined,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
82
|
+
const result = (await stripe.confirmPayment({
|
|
83
|
+
elements,
|
|
84
|
+
redirect: "if_required",
|
|
126
85
|
}));
|
|
127
86
|
if (result.error) {
|
|
128
87
|
// This point will only be reached if there is an immediate error.
|
|
@@ -169,6 +128,16 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
169
128
|
if (!stripeReady) {
|
|
170
129
|
return _jsx(StripeLoadingState, {});
|
|
171
130
|
}
|
|
131
|
+
const stripeElementOptions = {
|
|
132
|
+
layout: "tabs",
|
|
133
|
+
fields: {
|
|
134
|
+
billingDetails: "auto",
|
|
135
|
+
},
|
|
136
|
+
wallets: {
|
|
137
|
+
applePay: "auto",
|
|
138
|
+
googlePay: "auto",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
172
141
|
const howItWorksSteps = [
|
|
173
142
|
{
|
|
174
143
|
number: 1,
|
|
@@ -183,19 +152,20 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
|
|
|
183
152
|
description: "After payment confirmation, your order will be processed and completed automatically",
|
|
184
153
|
},
|
|
185
154
|
];
|
|
186
|
-
return (_jsxs("div", { className: "relative mt-1 flex w-full flex-1 flex-col items-center justify-center", children: [_jsxs("form", { onSubmit: handleSubmit, className: "w-full space-y-6", children: [_jsx(OrderDetailsCollapsible, { order: order, dstToken: order.metadata.dstToken, tournament: order.type === "join_tournament" || order.type === "fund_tournament" ? order.metadata.tournament : undefined, nft: order.type === "mint_nft" ? order.metadata.nft : undefined, recipientName: recipientName, formattedExpectedDstAmount: formatTokenAmount(BigInt(order.srcAmount), order.metadata.dstToken.decimals), showTotal: true, totalAmount: amount ? `$${Number(amount).toFixed(2)}` : undefined }), _jsxs("div", { className: "w-full", children: [_jsx("div", { className: "text-as-primary mb-4 text-lg font-semibold", children: "Payment Details" }), _jsx(
|
|
155
|
+
return (_jsxs("div", { className: "relative mt-1 flex w-full flex-1 flex-col items-center justify-center", children: [_jsxs("form", { onSubmit: handleSubmit, className: "w-full space-y-6", children: [_jsx(OrderDetailsCollapsible, { order: order, dstToken: order.metadata.dstToken, tournament: order.type === "join_tournament" || order.type === "fund_tournament" ? order.metadata.tournament : undefined, nft: order.type === "mint_nft" ? order.metadata.nft : undefined, recipientName: recipientName, formattedExpectedDstAmount: formatTokenAmount(BigInt(order.srcAmount), order.metadata.dstToken.decimals), showTotal: true, totalAmount: amount ? `$${Number(amount).toFixed(2)}` : undefined }), _jsxs("div", { className: "w-full", children: [_jsx("div", { className: "text-as-primary mb-4 text-lg font-semibold", children: "Payment Details" }), _jsx(PaymentElement, { onChange: handlePaymentElementChange, options: stripeElementOptions }), showAddressElement && (_jsx(AddressElement, { options: {
|
|
187
156
|
mode: "billing",
|
|
188
|
-
fields: {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
157
|
+
fields: {
|
|
158
|
+
phone: "always",
|
|
159
|
+
},
|
|
160
|
+
// More granular control
|
|
161
|
+
display: {
|
|
162
|
+
name: "split", // or 'split' for first/last name separately
|
|
163
|
+
},
|
|
164
|
+
// Validation
|
|
165
|
+
validation: {
|
|
166
|
+
phone: {
|
|
167
|
+
required: "always", // or 'always', 'never'
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
} }))] }), message && (_jsxs("div", { className: "bg-as-red/10 border-as-red/20 flex w-full items-center gap-3 rounded-2xl border p-4", children: [_jsx("div", { className: "bg-as-red flex h-6 w-6 shrink-0 items-center justify-center rounded-full", children: _jsx("svg", { className: "h-4 w-4 text-white", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) }), _jsx("div", { className: "text-as-red text-sm font-medium", children: message })] })), _jsx(ShinyButton, { type: "submit", accentColor: "hsl(var(--as-brand))", disabled: !stripe || !elements || loading, className: "relative w-full py-4 text-lg font-semibold", children: loading ? (_jsxs("div", { className: "flex items-center justify-center gap-3", children: [_jsx("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-current border-t-transparent" }), _jsx("span", { className: "text-white", children: "Processing Payment..." })] })) : (_jsxs("div", { className: "flex items-center justify-center gap-2", children: [_jsx("span", { className: "text-white", children: "Complete Payment" }), amount && _jsxs("span", { className: "text-white/90", children: ["$", Number(amount).toFixed(2)] })] })) })] }), showHowItWorks && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", children: _jsxs("div", { className: "bg-as-on-surface-1 relative max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-2xl p-6", children: [_jsxs("div", { className: "mb-6 flex items-center justify-between", children: [_jsx("h2", { className: "text-as-primary text-xl font-semibold", children: "How it works" }), _jsx("button", { onClick: () => setShowHowItWorks(false), className: "text-as-primary/60 hover:text-as-primary transition-colors", children: _jsx(X, { className: "h-6 w-6" }) })] }), _jsxs("div", { className: "space-y-6", children: [_jsx(PaymentMethodIcons, {}), _jsx(HowItWorks, { steps: howItWorksSteps })] })] }) }))] }));
|
|
201
171
|
}
|
package/package.json
CHANGED
|
@@ -4,10 +4,10 @@ import { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
|
4
4
|
import { ShinyButton, useB3, useModalStore, useProfile } from "@b3dotfun/sdk/global-account/react";
|
|
5
5
|
import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
|
|
6
6
|
import { formatStripeAmount } from "@b3dotfun/sdk/shared/utils/payment.utils";
|
|
7
|
-
import { AddressElement,
|
|
8
|
-
import { loadStripe, PaymentIntentResult,
|
|
7
|
+
import { AddressElement, Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
|
8
|
+
import { loadStripe, PaymentIntentResult, StripePaymentElementOptions } from "@stripe/stripe-js";
|
|
9
9
|
import { X } from "lucide-react";
|
|
10
|
-
import {
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
11
|
import { AnySpendFingerprintWrapper, getFingerprintConfig } from "../AnySpendFingerprintWrapper";
|
|
12
12
|
import HowItWorks from "./HowItWorks";
|
|
13
13
|
import PaymentMethodIcons from "./PaymentMethodIcons";
|
|
@@ -99,17 +99,7 @@ function StripePaymentForm({
|
|
|
99
99
|
const [amount, setAmount] = useState<string | null>(null);
|
|
100
100
|
const [stripeReady, setStripeReady] = useState<boolean>(false);
|
|
101
101
|
const [showHowItWorks, setShowHowItWorks] = useState<boolean>(false);
|
|
102
|
-
|
|
103
|
-
const [cardComplete, setCardComplete] = useState<boolean>(false);
|
|
104
|
-
const [addressComplete, setAddressComplete] = useState<boolean>(false);
|
|
105
|
-
// Snapshot of AddressElement value to pass to billing_details
|
|
106
|
-
const [addressValue, setAddressValue] = useState<any>(null);
|
|
107
|
-
|
|
108
|
-
// Helper function to mark "How it works" as seen
|
|
109
|
-
const markHowItWorksAsSeen = useCallback(() => {
|
|
110
|
-
setShowHowItWorks(false);
|
|
111
|
-
localStorage.setItem("b3-payment-how-it-works-seen", "true");
|
|
112
|
-
}, []);
|
|
102
|
+
const [showAddressElement, setShowAddressElement] = useState<boolean>(false);
|
|
113
103
|
|
|
114
104
|
useEffect(() => {
|
|
115
105
|
if (stripe && elements) {
|
|
@@ -117,22 +107,16 @@ function StripePaymentForm({
|
|
|
117
107
|
}
|
|
118
108
|
}, [stripe, elements, order.id]);
|
|
119
109
|
|
|
120
|
-
// Check if user has seen "How it works" before
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
const hasSeenHowItWorks = localStorage.getItem("b3-payment-how-it-works-seen");
|
|
123
|
-
if (!hasSeenHowItWorks) {
|
|
124
|
-
setShowHowItWorks(true);
|
|
125
|
-
}
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
110
|
useEffect(() => {
|
|
129
111
|
const fetchPaymentIntent = async () => {
|
|
130
112
|
if (!stripe || !clientSecret) return;
|
|
131
113
|
|
|
132
114
|
try {
|
|
133
115
|
const paymentIntent = await stripe.retrievePaymentIntent(clientSecret);
|
|
134
|
-
const
|
|
135
|
-
|
|
116
|
+
const amount = paymentIntent.paymentIntent?.amount
|
|
117
|
+
? formatStripeAmount(paymentIntent.paymentIntent.amount)
|
|
118
|
+
: null;
|
|
119
|
+
setAmount(amount);
|
|
136
120
|
} catch (error) {
|
|
137
121
|
console.error("@@stripe-web2-payment:retrieve-intent-error:", JSON.stringify(error, null, 2));
|
|
138
122
|
}
|
|
@@ -141,10 +125,16 @@ function StripePaymentForm({
|
|
|
141
125
|
fetchPaymentIntent();
|
|
142
126
|
}, [clientSecret, stripe]);
|
|
143
127
|
|
|
128
|
+
// Handle payment element changes
|
|
129
|
+
const handlePaymentElementChange = (event: any) => {
|
|
130
|
+
// Show address element only for card payments
|
|
131
|
+
setShowAddressElement(event.value.type === "card");
|
|
132
|
+
};
|
|
133
|
+
|
|
144
134
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
145
135
|
e.preventDefault();
|
|
146
136
|
|
|
147
|
-
if (!stripe || !elements
|
|
137
|
+
if (!stripe || !elements) {
|
|
148
138
|
setMessage("Stripe is not initialized");
|
|
149
139
|
return;
|
|
150
140
|
}
|
|
@@ -152,44 +142,10 @@ function StripePaymentForm({
|
|
|
152
142
|
setLoading(true);
|
|
153
143
|
setMessage(null);
|
|
154
144
|
|
|
155
|
-
// Block submission until both card and billing address are complete
|
|
156
|
-
if (!cardComplete || !addressComplete) {
|
|
157
|
-
setMessage("Please complete all required billing address and card fields.");
|
|
158
|
-
setLoading(false);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
145
|
try {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
setLoading(false);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const result = (await stripe.confirmCardPayment(clientSecret, {
|
|
171
|
-
payment_method: {
|
|
172
|
-
card,
|
|
173
|
-
billing_details: {
|
|
174
|
-
// Map AddressElement values into billing_details
|
|
175
|
-
name: addressValue?.name
|
|
176
|
-
? typeof addressValue.name === "string"
|
|
177
|
-
? addressValue.name
|
|
178
|
-
: [addressValue.name.firstName, addressValue.name.lastName].filter(Boolean).join(" ")
|
|
179
|
-
: undefined,
|
|
180
|
-
phone: addressValue?.phone || undefined,
|
|
181
|
-
address: addressValue?.address
|
|
182
|
-
? {
|
|
183
|
-
line1: addressValue.address.line1 || undefined,
|
|
184
|
-
line2: addressValue.address.line2 || undefined,
|
|
185
|
-
city: addressValue.address.city || undefined,
|
|
186
|
-
state: addressValue.address.state || undefined, // province/region
|
|
187
|
-
postal_code: addressValue.address.postal_code || undefined,
|
|
188
|
-
country: addressValue.address.country || undefined,
|
|
189
|
-
}
|
|
190
|
-
: undefined,
|
|
191
|
-
},
|
|
192
|
-
},
|
|
146
|
+
const result = (await stripe.confirmPayment({
|
|
147
|
+
elements,
|
|
148
|
+
redirect: "if_required",
|
|
193
149
|
})) as PaymentIntentResult;
|
|
194
150
|
|
|
195
151
|
if (result.error) {
|
|
@@ -248,6 +204,17 @@ function StripePaymentForm({
|
|
|
248
204
|
return <StripeLoadingState />;
|
|
249
205
|
}
|
|
250
206
|
|
|
207
|
+
const stripeElementOptions: StripePaymentElementOptions = {
|
|
208
|
+
layout: "tabs" as const,
|
|
209
|
+
fields: {
|
|
210
|
+
billingDetails: "auto" as const,
|
|
211
|
+
},
|
|
212
|
+
wallets: {
|
|
213
|
+
applePay: "auto" as const,
|
|
214
|
+
googlePay: "auto" as const,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
251
218
|
const howItWorksSteps = [
|
|
252
219
|
{
|
|
253
220
|
number: 1,
|
|
@@ -279,28 +246,30 @@ function StripePaymentForm({
|
|
|
279
246
|
totalAmount={amount ? `$${Number(amount).toFixed(2)}` : undefined}
|
|
280
247
|
/>
|
|
281
248
|
|
|
282
|
-
{/* Simplified Payment Form
|
|
249
|
+
{/* Simplified Payment Form */}
|
|
283
250
|
<div className="w-full">
|
|
284
251
|
<div className="text-as-primary mb-4 text-lg font-semibold">Payment Details</div>
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
252
|
+
<PaymentElement onChange={handlePaymentElementChange} options={stripeElementOptions} />
|
|
253
|
+
{showAddressElement && (
|
|
254
|
+
<AddressElement
|
|
255
|
+
options={{
|
|
256
|
+
mode: "billing",
|
|
257
|
+
fields: {
|
|
258
|
+
phone: "always",
|
|
259
|
+
},
|
|
260
|
+
// More granular control
|
|
261
|
+
display: {
|
|
262
|
+
name: "split", // or 'split' for first/last name separately
|
|
263
|
+
},
|
|
264
|
+
// Validation
|
|
265
|
+
validation: {
|
|
266
|
+
phone: {
|
|
267
|
+
required: "always", // or 'always', 'never'
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
}}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
304
273
|
</div>
|
|
305
274
|
|
|
306
275
|
{/* Error Message */}
|
|
@@ -319,7 +288,7 @@ function StripePaymentForm({
|
|
|
319
288
|
<ShinyButton
|
|
320
289
|
type="submit"
|
|
321
290
|
accentColor="hsl(var(--as-brand))"
|
|
322
|
-
disabled={!stripe || !elements || loading
|
|
291
|
+
disabled={!stripe || !elements || loading}
|
|
323
292
|
className="relative w-full py-4 text-lg font-semibold"
|
|
324
293
|
>
|
|
325
294
|
{loading ? (
|
|
@@ -338,20 +307,13 @@ function StripePaymentForm({
|
|
|
338
307
|
|
|
339
308
|
{/* How it works modal */}
|
|
340
309
|
{showHowItWorks && (
|
|
341
|
-
<div
|
|
342
|
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
343
|
-
onClick={e => {
|
|
344
|
-
if (e.target === e.currentTarget) {
|
|
345
|
-
markHowItWorksAsSeen();
|
|
346
|
-
}
|
|
347
|
-
}}
|
|
348
|
-
>
|
|
310
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
|
349
311
|
<div className="bg-as-on-surface-1 relative max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-2xl p-6">
|
|
350
312
|
{/* Modal header */}
|
|
351
313
|
<div className="mb-6 flex items-center justify-between">
|
|
352
314
|
<h2 className="text-as-primary text-xl font-semibold">How it works</h2>
|
|
353
315
|
<button
|
|
354
|
-
onClick={
|
|
316
|
+
onClick={() => setShowHowItWorks(false)}
|
|
355
317
|
className="text-as-primary/60 hover:text-as-primary transition-colors"
|
|
356
318
|
>
|
|
357
319
|
<X className="h-6 w-6" />
|