Skip to content

Commit d153a95

Browse files
authored
Merge branch 'main' into upstream-message-examples
2 parents beb5b68 + 7c77f3f commit d153a95

File tree

11 files changed

+369
-6
lines changed

11 files changed

+369
-6
lines changed

.env.sample

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33

44
PAYPAL_SANDBOX_CLIENT_ID=
55
PAYPAL_SANDBOX_CLIENT_SECRET=
6+
7+
# Provide a comma-separated list of the domain names where
8+
# the Fastlane component will be presented
9+
# ex: DOMAINS=your-domain.com,your-domain2.com
10+
DOMAINS=
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20.18.1
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Fastlane HTML Sample Integration
2+
3+
This sample demonstrates how to integrate PayPal Fastlane using plain HTML, JavaScript, and CSS. No build process is required—just static files that can be served by any web server. [Vite](https://vite.dev/) is used locally to serve the files and proxy API requests.
4+
5+
## How to Run Locally
6+
7+
```bash
8+
npm install
9+
npm start
10+
```
11+
12+
- The Fastlane demo will be available at [http://localhost:3000](http://localhost:3000).
13+
- The backend API server (see instructions in `/server/node/README.md`) must also be running on [http://localhost:8080](http://localhost:8080).
14+
15+
## Features
16+
17+
- **Email Lookup:** Enter an email to check if the user is a Fastlane member.
18+
- **Member Experience:** Authenticated members can select shipping addresses and pay with saved cards.
19+
- **Guest Experience:** Guests can enter card details and shipping info.
20+
- **Order Creation:** Submits a PayPal order using the Fastlane payment token.
21+
- **Card Testing Info:** Provides a link to PayPal's sandbox card testing documentation.
22+
23+
## File Structure
24+
25+
- [`src/index.html`](src/index.html): Main HTML page for the Fastlane demo.
26+
- [`src/fastlaneSdkComponent.js`](src/fastlaneSdkComponent.js): Loads the PayPal SDK and initializes Fastlane.
27+
- [`src/fastlane.js`](src/fastlane.js): Handles Fastlane logic, UI updates, and order creation.
28+
- [`src/fastlane.css`](src/fastlane.css): Basic styles for the demo.
29+
30+
## How It Works
31+
32+
1. The page loads the PayPal v6 Web SDK with the Fastlane component.
33+
2. The user enters their email and submits the form.
34+
3. The app checks if the user is a Fastlane member:
35+
- **Member:** Authenticates and displays saved shipping/payment info.
36+
- **Guest:** Prompts for card entry.
37+
4. On order submission, a payment token is generated and sent to the backend to create a PayPal order.
38+
39+
## API Endpoints Used
40+
41+
- `GET /paypal-api/auth/browser-safe-client-token`
42+
- `POST /paypal-api/checkout/orders/create`
43+
44+
> **Note:** The backend API server must be running and configured with your PayPal sandbox credentials (please see the `.env.sample` at the root folder).
45+
46+
## Resources
47+
48+
- [PayPal Fastlane Documentation](https://developer.paypal.com/studio/checkout/fastlane)
49+
- [PayPal Card Testing](https://developer.paypal.com/tools/sandbox/card-testing/)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "v6-web-sdk-sample-integration-client-html",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"start": "vite",
10+
"format": "prettier . --write",
11+
"format:check": "prettier . --check",
12+
"lint": "echo \"eslint is not set up\""
13+
},
14+
"license": "Apache-2.0",
15+
"devDependencies": {
16+
"prettier": "^3.5.3",
17+
"vite": "^6.2.0"
18+
}
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#email-input {
2+
width: 15rem;
3+
}
4+
5+
#shipping-display-container {
6+
border: 1px solid black;
7+
padding: 1rem;
8+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
async function setupFastlaneSdk() {
2+
fastlane.setLocale("en_us");
3+
4+
const fastlaneWatermark = await fastlane.FastlaneWatermarkComponent({
5+
includeAdditionalInfo: true,
6+
});
7+
fastlaneWatermark.render("#watermark-container");
8+
9+
const emailInput = document.getElementById("email-input");
10+
const emailSubmitButton = document.getElementById("email-submit-button");
11+
emailSubmitButton.addEventListener("click", async (e) => {
12+
e.preventDefault();
13+
14+
const { customerContextId } = await fastlane.identity.lookupCustomerByEmail(
15+
emailInput.value,
16+
);
17+
18+
let shouldRenderFastlaneMemberExperience = false;
19+
let profileData;
20+
if (customerContextId) {
21+
const response =
22+
await fastlane.identity.triggerAuthenticationFlow(customerContextId);
23+
24+
if (response.authenticationState === "succeeded") {
25+
shouldRenderFastlaneMemberExperience = true;
26+
profileData = response.profileData;
27+
}
28+
}
29+
30+
const emailForm = document.getElementById("email-form");
31+
emailForm.setAttribute("hidden", true);
32+
33+
const submitOrderButton = document.getElementById("submit-button");
34+
submitOrderButton.removeAttribute("hidden");
35+
36+
if (shouldRenderFastlaneMemberExperience) {
37+
renderFastlaneMemberExperience(profileData);
38+
} else {
39+
renderFastlaneGuestExperience();
40+
}
41+
});
42+
}
43+
44+
function setShippingAddressDisplay(shippingAddress) {
45+
const {
46+
name: { fullName },
47+
address: { addressLine1, adminArea2, adminArea1, postalCode },
48+
} = shippingAddress;
49+
const shippingDisplayContainer = document.getElementById(
50+
"shipping-display-container",
51+
);
52+
shippingDisplayContainer.removeAttribute("hidden");
53+
shippingDisplayContainer.innerHTML = `<b>${fullName}</b><br><b>${adminArea2}</b><br><b>${adminArea1}</b><br><b>${postalCode}</b>`;
54+
}
55+
56+
async function renderFastlaneMemberExperience(profileData) {
57+
if (profileData.shippingAddress) {
58+
setShippingAddressDisplay(profileData.shippingAddress);
59+
60+
const changeAddressButton = document.getElementById(
61+
"change-shipping-button",
62+
);
63+
64+
changeAddressButton.removeAttribute("hidden");
65+
changeAddressButton.addEventListener("click", async () => {
66+
const { selectedAddress, selectionChanged } =
67+
await fastlane.profile.showShippingAddressSelector();
68+
69+
if (selectionChanged) {
70+
profileData.shippingAddress = selectedAddress;
71+
setShippingAddressDisplay(profileData.shippingAddress);
72+
}
73+
});
74+
75+
const fastlanePaymentComponent = await fastlane.FastlanePaymentComponent({
76+
options: {},
77+
shippingAddress: profileData.shippingAddress,
78+
});
79+
80+
fastlanePaymentComponent.render("#payment-container");
81+
82+
const submitButton = document.getElementById("submit-button");
83+
submitButton.addEventListener("click", async () => {
84+
const { id } = await fastlanePaymentComponent.getPaymentToken();
85+
86+
const orderResponse = await createOrder(id);
87+
console.log("orderResponse: ", orderResponse);
88+
89+
if (orderResponse.status === "COMPLETED") {
90+
alert("Order completed successfully! Check console for details.");
91+
} else {
92+
alert("There was an issue processing your order. Please try again.");
93+
}
94+
});
95+
} else {
96+
// Render your shipping address form
97+
}
98+
}
99+
100+
async function renderFastlaneGuestExperience() {
101+
const cardTestingInfo = document.getElementById("card-testing-info");
102+
cardTestingInfo.removeAttribute("hidden");
103+
104+
const FastlanePaymentComponent = await fastlane.FastlanePaymentComponent({});
105+
await FastlanePaymentComponent.render("#card-container");
106+
107+
const submitButton = document.getElementById("submit-button");
108+
submitButton.addEventListener("click", async () => {
109+
const { id } = await FastlanePaymentComponent.getPaymentToken();
110+
111+
const orderResponse = await createOrder(id);
112+
console.log("orderResponse: ", orderResponse);
113+
114+
if (orderResponse.status === "COMPLETED") {
115+
alert("Order completed successfully! Check console for details.");
116+
} else {
117+
alert("There was an issue processing your order. Please try again.");
118+
}
119+
});
120+
}
121+
122+
async function createOrder(paymentToken) {
123+
const response = await fetch("/paypal-api/checkout/orders/create", {
124+
method: "POST",
125+
headers: {
126+
"Content-Type": "application/json",
127+
"PayPal-Request-Id": Date.now().toString(),
128+
},
129+
body: JSON.stringify({
130+
paymentSource: {
131+
card: {
132+
singleUseToken: paymentToken,
133+
},
134+
},
135+
purchaseUnits: [
136+
{
137+
amount: {
138+
currencyCode: "USD",
139+
value: "10.00",
140+
breakdown: {
141+
itemTotal: {
142+
currencyCode: "USD",
143+
value: "10.00",
144+
},
145+
},
146+
},
147+
},
148+
],
149+
intent: "CAPTURE",
150+
}),
151+
});
152+
const orderResponse = await response.json();
153+
154+
return orderResponse;
155+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
let fastlane;
2+
async function onPayPalLoaded() {
3+
const clientToken = await getBrowserSafeClientToken();
4+
5+
const sdkInstance = await window.paypal.createInstance({
6+
clientToken,
7+
pageType: "product-details",
8+
clientMetadataId: crypto.randomUUID(),
9+
components: ["fastlane"],
10+
});
11+
12+
fastlane = await sdkInstance.createFastlane();
13+
setupFastlaneSdk();
14+
}
15+
16+
async function getBrowserSafeClientToken() {
17+
const response = await fetch("/paypal-api/auth/browser-safe-client-token", {
18+
method: "GET",
19+
headers: {
20+
"Content-Type": "application/json",
21+
},
22+
});
23+
const { accessToken } = await response.json();
24+
25+
return accessToken;
26+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<link href="fastlane.css" rel="stylesheet" />
7+
</head>
8+
<body>
9+
<div>
10+
<form id="email-form">
11+
<fieldset>
12+
<legend>Email</legend>
13+
<input type="email" id="email-input" />
14+
<div id="watermark-container">
15+
<img
16+
src="https://www.paypalobjects.com/fastlane-v1/assets/fastlane-with-tooltip_en_sm_light.0808.svg"
17+
/>
18+
</div>
19+
20+
<button id="email-submit-button">Submit</button>
21+
</fieldset>
22+
</form>
23+
24+
<p id="card-testing-info" hidden>
25+
For more info on testing cards with PayPal, see
26+
<a href="https://developer.paypal.com/tools/sandbox/card-testing/"
27+
>https://developer.paypal.com/tools/sandbox/card-testing/</a
28+
>.
29+
</p>
30+
<div id="card-container"></div>
31+
32+
<div id="shipping-display-container" hidden></div>
33+
<button id="change-shipping-button" hidden>
34+
Change Shipping Address
35+
</button>
36+
37+
<div id="payment-container"></div>
38+
<button id="submit-button" hidden>Submit Order</button>
39+
</div>
40+
41+
<script src="fastlaneSdkComponent.js"></script>
42+
<script src="fastlane.js"></script>
43+
<script
44+
async
45+
onload="onPayPalLoaded()"
46+
src="https://www.sandbox.paypal.com/web-sdk/v6/core"
47+
></script>
48+
</body>
49+
</html>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { defineConfig } from "vite";
2+
3+
// https://vitejs.dev/config/
4+
export default defineConfig({
5+
plugins: [],
6+
root: "src",
7+
server: {
8+
port: 3000,
9+
proxy: {
10+
"/paypal-api": {
11+
target: "http://localhost:8080",
12+
changeOrigin: true,
13+
secure: false,
14+
},
15+
},
16+
},
17+
});

0 commit comments

Comments
 (0)