Skip to content

Commit 6ca468d

Browse files
Feat: React Vault with Purchase (#182)
* refactor save payment method example * add vault with purchase flow and update index with better favicon * run prettier * add response.ok conditions to react api helpers
1 parent b612ead commit 6ca468d

File tree

10 files changed

+492
-148
lines changed

10 files changed

+492
-148
lines changed

client/prebuiltPages/react/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link
7+
rel="icon"
8+
href="https://www.paypalobjects.com/marketing/web/logos/paypal-mark-color_new.svg"
9+
/>
610
<title>PayPal V6 Web SDK - Checkout Demos</title>
711
</head>
812
<body>

client/prebuiltPages/react/src/App.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import BaseCart from "./pages/BaseCart";
1111
// One-Time Payment flow
1212
import OneTimeCheckoutPage from "./paymentFlowCheckoutPages/OneTimePaymentCheckout";
1313

14+
// One-Time Payment with Vault flow
15+
import VaultWithPurchaseCheckoutPage from "./paymentFlowCheckoutPages/VaultWithPurchaseCheckout";
16+
1417
// Save Payment flow
15-
import SavePaymentCheckoutPage from "./paymentFlowCheckoutPages/SavePaymentCheckout";
18+
import SavePaymentSettings from "./pages/SavePaymentSettings";
1619

1720
// Subscription flow
1821
import SubscriptionCheckoutPage from "./paymentFlowCheckoutPages/SubscriptionCheckout";
@@ -123,24 +126,27 @@ function App() {
123126
element={<ErrorBoundaryTestPage />}
124127
/>
125128

126-
{/* Save Payment flow */}
129+
{/* One-Time Payment with Vault flow */}
127130
<Route
128-
path="/save-payment"
129-
element={<BaseProduct flowType="save-payment" />}
131+
path="/vault-with-purchase"
132+
element={<BaseProduct flowType="vault-with-purchase" />}
130133
/>
131134
<Route
132-
path="/save-payment/cart"
133-
element={<BaseCart flowType="save-payment" />}
135+
path="/vault-with-purchase/cart"
136+
element={<BaseCart flowType="vault-with-purchase" />}
134137
/>
135138
<Route
136-
path="/save-payment/checkout"
137-
element={<SavePaymentCheckoutPage />}
139+
path="/vault-with-purchase/checkout"
140+
element={<VaultWithPurchaseCheckoutPage />}
138141
/>
139142
<Route
140-
path="/save-payment/error"
143+
path="/vault-with-purchase/error"
141144
element={<ErrorBoundaryTestPage />}
142145
/>
143146

147+
{/* Save Payment flow */}
148+
<Route path="/save-payment" element={<SavePaymentSettings />} />
149+
144150
{/* Subscription flow */}
145151
<Route
146152
path="/subscription"

client/prebuiltPages/react/src/pages/BaseCart.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { useCartTotals } from "../hooks/useCartTotals";
55
import "../styles/Cart.css";
66

77
interface CartPageProps {
8-
flowType: "one-time-payment" | "save-payment" | "subscription";
8+
flowType:
9+
| "one-time-payment"
10+
| "save-payment"
11+
| "subscription"
12+
| "vault-with-purchase";
913
}
1014

1115
const BaseCart = ({ flowType }: CartPageProps) => {

client/prebuiltPages/react/src/pages/BaseCheckout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { useCartTotals } from "../hooks/useCartTotals";
66
import "../styles/Checkout.css";
77

88
interface CheckoutPageProps {
9-
flowType: "one-time-payment" | "save-payment" | "subscription";
9+
flowType:
10+
| "one-time-payment"
11+
| "save-payment"
12+
| "subscription"
13+
| "vault-with-purchase";
1014
modalState: ModalType;
1115
onModalClose: () => void;
1216
getModalContent: (state: ModalType) => ModalContent | null;

client/prebuiltPages/react/src/pages/BaseProduct.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { useQuantityChange } from "../hooks/useQuantityChange";
55
import "../styles/Product.css";
66

77
interface ProductPageProps {
8-
flowType: "one-time-payment" | "save-payment" | "subscription";
8+
flowType:
9+
| "one-time-payment"
10+
| "save-payment"
11+
| "subscription"
12+
| "vault-with-purchase";
913
}
1014

1115
const BaseProduct = ({ flowType }: ProductPageProps) => {

client/prebuiltPages/react/src/pages/Home.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,40 @@ export function HomePage() {
9090
to payment.
9191
</td>
9292
</tr>
93+
<tr
94+
style={{
95+
borderBottom: "1px solid #e2e8f0",
96+
}}
97+
>
98+
<td
99+
style={{
100+
padding: "16px",
101+
}}
102+
>
103+
<Link
104+
to="/vault-with-purchase"
105+
style={{
106+
color: "#0070ba",
107+
textDecoration: "none",
108+
fontSize: "16px",
109+
fontWeight: "500",
110+
}}
111+
>
112+
Vault with Purchase
113+
</Link>
114+
</td>
115+
<td
116+
style={{
117+
padding: "16px",
118+
color: "#4a5568",
119+
fontSize: "14px",
120+
lineHeight: "1.6",
121+
}}
122+
>
123+
Complete a purchase and save the payment method for future
124+
use. Demonstrates the vault-with-purchase flow.
125+
</td>
126+
</tr>
93127
<tr
94128
style={{
95129
borderBottom: "1px solid #e2e8f0",
@@ -120,8 +154,8 @@ export function HomePage() {
120154
lineHeight: "1.6",
121155
}}
122156
>
123-
Vault payment methods for future use. Shows how to securely
124-
save customer payment information for recurring purchases.
157+
Save a payment method without making a purchase. Demonstrates
158+
the vault-without-payment flow in an account settings context.
125159
</td>
126160
</tr>
127161
<tr
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { useState, useCallback } from "react";
2+
import {
3+
usePayPal,
4+
useEligibleMethods,
5+
INSTANCE_LOADING_STATE,
6+
type OnApproveDataSavePayments,
7+
type OnCompleteData,
8+
type OnErrorData,
9+
type OnCancelDataSavePayments,
10+
PayPalSavePaymentButton,
11+
PayPalCreditSavePaymentButton,
12+
} from "@paypal/react-paypal-js/sdk-v6";
13+
import PaymentModal from "../components/PaymentModal";
14+
import type { ModalType, ModalContent } from "../types";
15+
import { createVaultToken } from "../utils";
16+
17+
const SavePaymentSettings = () => {
18+
const [modalState, setModalState] = useState<ModalType>(null);
19+
const { loadingStatus } = usePayPal();
20+
21+
const { error: eligibilityError } = useEligibleMethods({
22+
payload: {
23+
currencyCode: "USD",
24+
paymentFlow: "VAULT_WITHOUT_PAYMENT",
25+
},
26+
});
27+
28+
const isLoading = loadingStatus === INSTANCE_LOADING_STATE.PENDING;
29+
30+
const handleSaveCallbacks = {
31+
onApprove: async (data: OnApproveDataSavePayments) => {
32+
console.log("Payment method saved:", data);
33+
console.log("Vault Setup Token:", data.vaultSetupToken);
34+
setModalState("success");
35+
},
36+
37+
onCancel: (data: OnCancelDataSavePayments) => {
38+
console.log("Save payment method cancelled:", data);
39+
setModalState("cancel");
40+
},
41+
42+
onError: (error: OnErrorData) => {
43+
console.error("Save payment method error:", error);
44+
setModalState("error");
45+
},
46+
47+
onComplete: (data: OnCompleteData) => {
48+
console.log("Payment session completed");
49+
console.log("On Complete data:", data);
50+
},
51+
};
52+
53+
const getModalContent = useCallback(
54+
(state: ModalType): ModalContent | null => {
55+
switch (state) {
56+
case "success":
57+
return {
58+
title: "Payment Method Saved!",
59+
message:
60+
"Your payment method has been securely saved for future use.",
61+
};
62+
case "cancel":
63+
return {
64+
title: "Save Cancelled",
65+
message: "Saving your payment method was cancelled.",
66+
};
67+
case "error":
68+
return {
69+
title: "Save Error",
70+
message:
71+
"There was an error saving your payment method. Please try again.",
72+
};
73+
default:
74+
return null;
75+
}
76+
},
77+
[],
78+
);
79+
80+
const handleModalClose = () => {
81+
setModalState(null);
82+
};
83+
84+
const modalContent = modalState ? getModalContent(modalState) : null;
85+
86+
return (
87+
<div
88+
style={{
89+
minHeight: "100vh",
90+
backgroundColor: "#f5f5f5",
91+
padding: "40px 20px",
92+
}}
93+
>
94+
<div style={{ maxWidth: "800px", margin: "0 auto" }}>
95+
<h1
96+
style={{
97+
fontSize: "24px",
98+
fontWeight: "600",
99+
color: "#1a1a2e",
100+
marginBottom: "24px",
101+
}}
102+
>
103+
Account Settings
104+
</h1>
105+
106+
{/* Account Information */}
107+
<div
108+
style={{
109+
backgroundColor: "white",
110+
borderRadius: "8px",
111+
padding: "24px",
112+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
113+
marginBottom: "24px",
114+
}}
115+
>
116+
<h2
117+
style={{
118+
fontSize: "18px",
119+
fontWeight: "600",
120+
color: "#1a1a2e",
121+
marginTop: 0,
122+
marginBottom: "16px",
123+
}}
124+
>
125+
Account Information
126+
</h2>
127+
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
128+
<div
129+
style={{
130+
width: "48px",
131+
height: "48px",
132+
borderRadius: "50%",
133+
backgroundColor: "#0070ba",
134+
color: "white",
135+
display: "flex",
136+
alignItems: "center",
137+
justifyContent: "center",
138+
fontSize: "18px",
139+
fontWeight: "600",
140+
flexShrink: 0,
141+
}}
142+
>
143+
JC
144+
</div>
145+
<div>
146+
<div
147+
style={{
148+
fontSize: "16px",
149+
fontWeight: "500",
150+
color: "#1a1a2e",
151+
}}
152+
>
153+
Jane Cooper
154+
</div>
155+
<div style={{ fontSize: "14px", color: "#4a5568" }}>
156+
jane.cooper@example.com
157+
</div>
158+
</div>
159+
</div>
160+
</div>
161+
162+
{/* Payment Methods */}
163+
<div
164+
style={{
165+
backgroundColor: "white",
166+
borderRadius: "8px",
167+
padding: "24px",
168+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
169+
}}
170+
>
171+
<h2
172+
style={{
173+
fontSize: "18px",
174+
fontWeight: "600",
175+
color: "#1a1a2e",
176+
marginTop: 0,
177+
marginBottom: "16px",
178+
}}
179+
>
180+
Payment Methods
181+
</h2>
182+
<p
183+
style={{
184+
fontSize: "14px",
185+
color: "#718096",
186+
marginBottom: "24px",
187+
}}
188+
>
189+
No payment methods saved yet.
190+
</p>
191+
192+
<h3
193+
style={{
194+
fontSize: "16px",
195+
fontWeight: "500",
196+
color: "#1a1a2e",
197+
marginBottom: "16px",
198+
}}
199+
>
200+
Add a Payment Method
201+
</h3>
202+
203+
{isLoading ? (
204+
<div style={{ padding: "1rem", textAlign: "center" }}>
205+
Loading payment methods...
206+
</div>
207+
) : eligibilityError ? (
208+
<div style={{ padding: "1rem", textAlign: "center", color: "red" }}>
209+
Failed to load payment options. Please refresh the page.
210+
</div>
211+
) : (
212+
<div
213+
style={{
214+
display: "flex",
215+
flexDirection: "column",
216+
gap: "12px",
217+
}}
218+
>
219+
<div>
220+
<PayPalSavePaymentButton
221+
createVaultToken={createVaultToken}
222+
presentationMode="auto"
223+
{...handleSaveCallbacks}
224+
/>
225+
</div>
226+
<div>
227+
<PayPalCreditSavePaymentButton
228+
createVaultToken={createVaultToken}
229+
presentationMode="auto"
230+
{...handleSaveCallbacks}
231+
/>
232+
</div>
233+
</div>
234+
)}
235+
</div>
236+
</div>
237+
238+
{modalContent && (
239+
<PaymentModal content={modalContent} onClose={handleModalClose} />
240+
)}
241+
</div>
242+
);
243+
};
244+
245+
export default SavePaymentSettings;

0 commit comments

Comments
 (0)