Skip to content

Commit dd40a69

Browse files
Feature: React paypal-messages Example (#165)
* add paypal-messages demos and routing * refactor learn more example * clean up comments and remove instance state tracking * chore: prettier * update based on messaging teams comments * use latest react-paypal-js alpha version
1 parent 7476278 commit dd40a69

File tree

6 files changed

+403
-9
lines changed

6 files changed

+403
-9
lines changed

client/prebuiltPages/react/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/prebuiltPages/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"typecheck": "tsc --build --noEmit"
1414
},
1515
"dependencies": {
16-
"@paypal/react-paypal-js": "^9.0.0-alpha.9",
16+
"@paypal/react-paypal-js": "^9.0.0-alpha.11",
1717
"react": "19.2.4",
1818
"react-dom": "19.2.4",
1919
"react-error-boundary": "6.1.0",

client/prebuiltPages/react/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import SubscriptionStaticButtonsDemo from "./payments/subscription/pages/StaticB
2323
// Error handling demo
2424
import ErrorBoundaryTestPage from "./pages/ErrorBoundary";
2525

26+
// PayPal Messages demo
27+
import PayPalMessagesDemo from "./pages/PayPalMessagesDemo";
28+
2629
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
2730
return (
2831
<div role="alert" style={{ padding: "20px", textAlign: "center" }}>
@@ -95,6 +98,7 @@ function App() {
9598
"venmo-payments",
9699
"paypal-guest-payments",
97100
"paypal-subscriptions",
101+
"paypal-messages",
98102
]}
99103
pageType="checkout"
100104
>
@@ -170,6 +174,9 @@ function App() {
170174
element={<ErrorBoundaryTestPage />}
171175
/>
172176

177+
{/* PayPal Messages demo */}
178+
<Route path="/paypal-messages" element={<PayPalMessagesDemo />} />
179+
173180
{/* Error handling demo */}
174181
<Route
175182
path="/error-boundary-test"
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
import React, {
2+
useState,
3+
useEffect,
4+
useRef,
5+
useCallback,
6+
useMemo,
7+
} from "react";
8+
import { usePayPalMessages } from "@paypal/react-paypal-js/sdk-v6";
9+
import type {
10+
LearnMore,
11+
LearnMoreOptions,
12+
PayPalMessagesElement,
13+
} from "@paypal/react-paypal-js/sdk-v6";
14+
15+
interface PayPalMessagesProps {
16+
amount?: string;
17+
}
18+
19+
/**
20+
* Basic PayPal Messages component using auto-bootstrap mode.
21+
*
22+
* With `auto-bootstrap={true}`, the `<paypal-message>` web component handles
23+
* fetching and rendering Pay Later content on its own — no manual fetch calls needed.
24+
* This is the simplest integration path for displaying Pay Later messaging.
25+
*
26+
* The `onPaypalMessageClick` event fires when a buyer clicks the "Learn More"
27+
* link within the message, which can be used to track analytics or trigger
28+
* custom behavior.
29+
*/
30+
export const PayPalMessages: React.FC<PayPalMessagesProps> = ({ amount }) => {
31+
const { error } = usePayPalMessages({});
32+
33+
return error ? null : (
34+
<paypal-message
35+
auto-bootstrap={true}
36+
amount={amount}
37+
currency-code="USD"
38+
buyer-country="US"
39+
text-color="BLACK"
40+
logo-type="MONOGRAM"
41+
logo-position="LEFT"
42+
onPaypalMessageClick={(e) => {
43+
console.log("User clicked Learn More:", e.detail.config);
44+
}}
45+
></paypal-message>
46+
);
47+
};
48+
49+
/**
50+
* Manual messaging component that fetches content programmatically.
51+
*
52+
* Use this approach over auto-bootstrap when you need control over when content
53+
* is fetched, want to customize fetch options per render, or need to react to
54+
* the content before displaying it (via the `onReady` callback).
55+
*
56+
* The two-step flow is:
57+
* 1. Call `handleFetchContent()` with styling and callback options
58+
* 2. Apply the returned content to the `<paypal-message>` element via `setContent()`
59+
*/
60+
export const ManualMessagingComponent: React.FC<PayPalMessagesProps> = ({
61+
amount,
62+
}) => {
63+
const containerRef = useRef<PayPalMessagesElement | null>(null);
64+
const { handleFetchContent, isReady } = usePayPalMessages({
65+
buyerCountry: "US",
66+
currencyCode: "USD",
67+
});
68+
69+
useEffect(() => {
70+
async function loadContent() {
71+
if (!isReady) {
72+
return;
73+
}
74+
75+
await handleFetchContent({
76+
amount,
77+
logoPosition: "INLINE",
78+
logoType: "WORDMARK",
79+
textColor: "BLACK",
80+
onReady: (content) => {
81+
const element = containerRef.current;
82+
if (element && element.setContent) {
83+
element.setContent(content);
84+
}
85+
},
86+
});
87+
}
88+
89+
loadContent();
90+
}, [amount, isReady, handleFetchContent]);
91+
92+
return <paypal-message ref={containerRef} />;
93+
};
94+
95+
interface LearnMoreDemoProps {
96+
initialAmount?: string;
97+
}
98+
99+
type LearnMoreInstances = {
100+
auto: LearnMore | undefined;
101+
modal: LearnMore | undefined;
102+
popup: LearnMore | undefined;
103+
redirect: LearnMore | undefined;
104+
};
105+
106+
/**
107+
* Learn More demo with programmatic control over all 4 presentation modes.
108+
*
109+
* Each mode determines how the financing details are displayed to the buyer:
110+
* - AUTO: defaults to MODAL, but falls back to POPUP if the content is not embeddable in an iframe
111+
* - MODAL: iframe overlay with accessibility features (tab trapping, close button)
112+
* - POPUP: opens in a new browser popup window
113+
* - REDIRECT: opens in a new browser tab via window.open()
114+
*
115+
* Instances support dynamic reconfiguration via `instance.update()`, used here
116+
* to keep messaging in sync when the cart amount changes.
117+
*
118+
* Callback hooks:
119+
* - onShow/onClose: fired when the Learn More experience opens or closes
120+
* - onApply: fired when the buyer clicks the "Apply Now" button in the modal
121+
* - onCalculate: fired when the buyer interacts with the payment calculator in the modal
122+
*/
123+
export const PayPalMessagesLearnMore: React.FC<LearnMoreDemoProps> = ({
124+
initialAmount = "50.00",
125+
}) => {
126+
const [amount, setAmount] = useState(initialAmount);
127+
128+
const { handleCreateLearnMore, error, isReady } = usePayPalMessages({
129+
buyerCountry: "US",
130+
currencyCode: "USD",
131+
});
132+
133+
const createLearnMoreInstance = useCallback(
134+
(mode: string) => {
135+
const options: LearnMoreOptions = {
136+
amount,
137+
presentationMode: mode as LearnMoreOptions["presentationMode"],
138+
onShow: (data) => {
139+
console.log(`${mode} onShow`, data);
140+
},
141+
onClose: (data) => {
142+
console.log(`${mode} onClose`, data);
143+
},
144+
onApply: (data) => {
145+
console.log(`${mode} onApply`, data);
146+
},
147+
onCalculate: (data) => {
148+
console.log(`${mode} onCalculate`, data);
149+
},
150+
};
151+
152+
return handleCreateLearnMore(options);
153+
},
154+
[amount, handleCreateLearnMore],
155+
);
156+
157+
const learnMoreInstances = useMemo<LearnMoreInstances>(() => {
158+
if (!isReady) {
159+
return {
160+
auto: undefined,
161+
modal: undefined,
162+
popup: undefined,
163+
redirect: undefined,
164+
};
165+
}
166+
167+
return {
168+
auto: createLearnMoreInstance("AUTO"),
169+
modal: createLearnMoreInstance("MODAL"),
170+
popup: createLearnMoreInstance("POPUP"),
171+
redirect: createLearnMoreInstance("REDIRECT"),
172+
};
173+
}, [isReady, createLearnMoreInstance]);
174+
175+
useEffect(() => {
176+
if (!isReady) return;
177+
178+
Object.entries(learnMoreInstances).forEach(async ([mode, instance]) => {
179+
if (instance) {
180+
await instance.update({
181+
amount,
182+
presentationMode:
183+
mode.toUpperCase() as LearnMoreOptions["presentationMode"],
184+
});
185+
console.log(
186+
`Updated ${mode.toUpperCase()} instance with amount ${amount}`,
187+
);
188+
}
189+
});
190+
}, [amount, learnMoreInstances, isReady]);
191+
192+
const openLearnMore = async (mode: string) => {
193+
const instance =
194+
learnMoreInstances[mode.toLowerCase() as keyof typeof learnMoreInstances];
195+
if (instance) {
196+
console.log(`Opening ${mode} Learn More`);
197+
try {
198+
await instance.open();
199+
console.log(`${mode} Learn More opened successfully`);
200+
} catch (err) {
201+
console.error(`Failed to open ${mode} Learn More`, err);
202+
}
203+
}
204+
};
205+
206+
if (error) {
207+
return (
208+
<div style={{ padding: "20px", color: "red" }}>
209+
Error: {error.message}
210+
</div>
211+
);
212+
}
213+
214+
if (!isReady) {
215+
return <div style={{ padding: "20px" }}>Loading PayPal Messages...</div>;
216+
}
217+
218+
return (
219+
<div style={{ maxWidth: "900px", margin: "0 auto", padding: "20px" }}>
220+
<h2>Learn More Demo</h2>
221+
222+
{/* Amount Control */}
223+
<section style={{ marginBottom: "40px" }}>
224+
<h3>Amount Control</h3>
225+
<label style={{ display: "block", marginBottom: "10px" }}>
226+
<strong>Amount: </strong>
227+
<input
228+
type="text"
229+
value={amount}
230+
onChange={(e) => setAmount(e.target.value)}
231+
style={{
232+
padding: "8px",
233+
border: "1px solid #ccc",
234+
borderRadius: "4px",
235+
marginLeft: "10px",
236+
width: "150px",
237+
}}
238+
/>
239+
</label>
240+
<p style={{ fontSize: "14px", color: "#666" }}>
241+
Current amount: ${amount}
242+
</p>
243+
</section>
244+
245+
{/* Presentation Mode Buttons */}
246+
<section
247+
style={{
248+
marginBottom: "40px",
249+
padding: "20px",
250+
border: "1px solid #ddd",
251+
borderRadius: "8px",
252+
}}
253+
>
254+
<h3>Programmatic Learn More Control</h3>
255+
<p style={{ fontSize: "14px", color: "#666", marginBottom: "20px" }}>
256+
Click each button to open the corresponding Learn More presentation
257+
mode. Each instance was created using handleCreateLearnMore().
258+
</p>
259+
260+
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
261+
{["AUTO", "MODAL", "POPUP", "REDIRECT"].map((mode) => (
262+
<button
263+
key={mode}
264+
onClick={() => openLearnMore(mode)}
265+
style={{
266+
padding: "12px 20px",
267+
backgroundColor: "#0070ba",
268+
color: "white",
269+
border: "none",
270+
borderRadius: "4px",
271+
cursor: "pointer",
272+
fontWeight: "bold",
273+
fontSize: "14px",
274+
}}
275+
>
276+
Open {mode} Learn More
277+
</button>
278+
))}
279+
</div>
280+
</section>
281+
</div>
282+
);
283+
};

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,40 @@ export function HomePage() {
158158
for membership sites, SaaS products, and subscription boxes.
159159
</td>
160160
</tr>
161+
<tr
162+
style={{
163+
borderBottom: "1px solid #e2e8f0",
164+
}}
165+
>
166+
<td
167+
style={{
168+
padding: "16px",
169+
}}
170+
>
171+
<Link
172+
to="/paypal-messages"
173+
style={{
174+
color: "#0070ba",
175+
textDecoration: "none",
176+
fontSize: "16px",
177+
fontWeight: "500",
178+
}}
179+
>
180+
PayPal Messages
181+
</Link>
182+
</td>
183+
<td
184+
style={{
185+
padding: "16px",
186+
color: "#4a5568",
187+
fontSize: "14px",
188+
lineHeight: "1.6",
189+
}}
190+
>
191+
PayPal Pay Later messaging demos showing auto-bootstrap,
192+
manual content fetching, and Learn More presentation modes.
193+
</td>
194+
</tr>
161195
<tr
162196
style={{
163197
borderBottom: "1px solid #e2e8f0",

0 commit comments

Comments
 (0)