Skip to content

Commit 95301a3

Browse files
authored
feat: add payment token functionality to savePayment flow (#43)
1 parent 441a8bd commit 95301a3

File tree

5 files changed

+137
-19
lines changed

5 files changed

+137
-19
lines changed

.github/workflows/nodejs.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ jobs:
2828
find . -name package.json -maxdepth 10 -type f -not -path "*/node_modules/*" | while read -r file; do
2929
directory=$(dirname "$file")
3030
echo "::group:: $directory"
31-
cd "$directory" && npm install && cd -
31+
cd "$directory"
32+
npm install || exit 1
33+
cd -
3234
echo "::endgroup::"
3335
done
3436
@@ -37,7 +39,9 @@ jobs:
3739
find . -name package.json -maxdepth 10 -type f -not -path "*/node_modules/*" | while read -r file; do
3840
directory=$(dirname "$file")
3941
echo "::group:: $directory"
40-
cd "$directory" && npm run format:check && cd -
42+
cd "$directory"
43+
npm run format:check || exit 1
44+
cd -
4145
echo "::endgroup::"
4246
done
4347
@@ -46,6 +50,8 @@ jobs:
4650
find . -name package.json -maxdepth 10 -type f -not -path "*/node_modules/*" | while read -r file; do
4751
directory=$(dirname "$file")
4852
echo "::group:: $directory"
49-
cd "$directory" && npm run lint && cd -
53+
cd "$directory"
54+
npm run lint || exit 1
55+
cd -
5056
echo "::endgroup::"
5157
done

client/savePayment/html/src/recommended/app.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ async function onPayPalLoaded() {
2323
const paymentSessionOptions = {
2424
async onApprove(data) {
2525
console.log("onApprove", data);
26+
const createPaymentTokenResponse = await createPaymentToken(
27+
data.vaultSetupToken,
28+
);
29+
console.log("Create payment token response: ", createPaymentTokenResponse);
2630
},
2731
onCancel(data) {
2832
console.log("onCancel", data);
@@ -65,7 +69,7 @@ async function getBrowserSafeClientToken() {
6569
}
6670

6771
async function createSetupToken() {
68-
const response = await fetch("/paypal-api/checkout/setup-token/create", {
72+
const response = await fetch("/paypal-api/vault/setup-token/create", {
6973
method: "POST",
7074
headers: {
7175
"Content-Type": "application/json",
@@ -75,3 +79,16 @@ async function createSetupToken() {
7579

7680
return { setupToken: id };
7781
}
82+
83+
async function createPaymentToken(vaultSetupToken) {
84+
const response = await fetch("/paypal-api/vault/payment-token/create", {
85+
method: "POST",
86+
headers: {
87+
"Content-Type": "application/json",
88+
},
89+
body: JSON.stringify({ vaultSetupToken }),
90+
});
91+
const data = await response.json();
92+
93+
return data;
94+
}

client/savePayment/html/src/recommended/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<h1>Save Payment Recommended Integration</h1>
1818

1919
<div class="buttons-container">
20-
<paypal-button id="paypal-button" type="pay" hidden></paypal-button>
20+
<paypal-button id="paypal-button" hidden></paypal-button>
2121
</div>
2222
<script src="app.js"></script>
2323

server/node/src/paypalServerSdk.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {
1616
PaypalPaymentTokenUsageType,
1717
VaultController,
1818
VaultInstructionAction,
19+
VaultTokenRequestType,
1920
} from "@paypal/paypal-server-sdk";
2021

2122
import type {
2223
OAuthProviderError,
23-
OAuthToken,
2424
OrderRequest,
25+
SetupTokenRequest,
2526
} from "@paypal/paypal-server-sdk";
2627

2728
/* ######################################################################
@@ -112,7 +113,7 @@ export async function getBrowserSafeClientToken() {
112113
}
113114

114115
/* ######################################################################
115-
* Process transactions
116+
* Process orders
116117
* ###################################################################### */
117118

118119
export async function createOrder(orderRequestBody: OrderRequest) {
@@ -178,19 +179,67 @@ export async function captureOrder(orderId: string) {
178179
}
179180
}
180181

181-
export async function createSetupToken() {
182+
/* ######################################################################
183+
* Save payment methods
184+
* ###################################################################### */
185+
186+
export async function createSetupToken(
187+
setupTokenRequestBody: SetupTokenRequest,
188+
paypalRequestId?: string,
189+
) {
182190
try {
183191
const { result, statusCode } = await vaultController.createSetupToken({
184-
paypalRequestId: Date.now().toString(),
192+
body: setupTokenRequestBody,
193+
paypalRequestId,
194+
});
195+
196+
return {
197+
jsonResponse: result,
198+
httpStatusCode: statusCode,
199+
};
200+
} catch (error) {
201+
if (error instanceof ApiError) {
202+
const { result, statusCode } = error;
203+
204+
return {
205+
jsonResponse: result as CustomError,
206+
httpStatusCode: statusCode,
207+
};
208+
} else {
209+
throw error;
210+
}
211+
}
212+
}
213+
214+
export async function createSetupTokenWithSampleDataForPayPal() {
215+
const defaultSetupTokenRequestBody = {
216+
paymentSource: {
217+
paypal: {
218+
experienceContext: {
219+
cancelUrl: "https://example.com/cancelUrl",
220+
returnUrl: "https://example.com/returnUrl",
221+
vaultInstruction: VaultInstructionAction.OnPayerApproval,
222+
},
223+
usageType: PaypalPaymentTokenUsageType.Merchant,
224+
},
225+
},
226+
};
227+
228+
return createSetupToken(defaultSetupTokenRequestBody, Date.now().toString());
229+
}
230+
231+
export async function createPaymentToken(
232+
vaultSetupToken: string,
233+
paypalRequestId?: string,
234+
) {
235+
try {
236+
const { result, statusCode } = await vaultController.createPaymentToken({
237+
paypalRequestId: paypalRequestId ?? Date.now().toString(),
185238
body: {
186239
paymentSource: {
187-
paypal: {
188-
experienceContext: {
189-
cancelUrl: "https://example.com/cancelUrl",
190-
returnUrl: "https://example.com/returnUrl",
191-
vaultInstruction: VaultInstructionAction.OnPayerApproval,
192-
},
193-
usageType: PaypalPaymentTokenUsageType.Merchant,
240+
token: {
241+
id: vaultSetupToken,
242+
type: VaultTokenRequestType.SetupToken,
194243
},
195244
},
196245
},

server/node/src/server.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import express, { Request, Response } from "express";
22
import cors from "cors";
33

4+
import type { PaymentTokenResponse } from "@paypal/paypal-server-sdk";
5+
46
import {
57
getBrowserSafeClientToken,
68
createOrderWithSampleData,
79
captureOrder,
8-
createSetupToken,
10+
createPaymentToken,
11+
createSetupTokenWithSampleDataForPayPal,
912
} from "./paypalServerSdk";
1013

1114
const app = express();
@@ -62,10 +65,11 @@ app.post(
6265
);
6366

6467
app.post(
65-
"/paypal-api/checkout/setup-token/create",
68+
"/paypal-api/vault/setup-token/create",
6669
async (_req: Request, res: Response) => {
6770
try {
68-
const { jsonResponse, httpStatusCode } = await createSetupToken();
71+
const { jsonResponse, httpStatusCode } =
72+
await createSetupTokenWithSampleDataForPayPal();
6973
res.status(httpStatusCode).json(jsonResponse);
7074
} catch (error) {
7175
console.error("Failed to create setup token:", error);
@@ -74,6 +78,48 @@ app.post(
7478
},
7579
);
7680

81+
app.post(
82+
"/paypal-api/vault/payment-token/create",
83+
async (req: Request, res: Response) => {
84+
try {
85+
const { jsonResponse, httpStatusCode } = await createPaymentToken(
86+
req.body.vaultSetupToken as string,
87+
);
88+
89+
const paymentTokenResponse = jsonResponse as PaymentTokenResponse;
90+
91+
if (paymentTokenResponse.id) {
92+
// This payment token id is a long-lived value for making
93+
// future payments when the buyer is not present.
94+
// PayPal recommends storing this value in your database
95+
// and NOT returning it back to the browser.
96+
await savePaymentTokenToDatabase(paymentTokenResponse);
97+
res.status(httpStatusCode).json({
98+
status: "SUCCESS",
99+
description:
100+
"Payment token saved to database for future transactions",
101+
});
102+
} else {
103+
res.status(httpStatusCode).json({
104+
status: "ERROR",
105+
description: "Failed to create payment token",
106+
});
107+
}
108+
} catch (error) {
109+
console.error("Failed to create payment token:", error);
110+
res.status(500).json({ error: "Failed to create payment token." });
111+
}
112+
},
113+
);
114+
115+
async function savePaymentTokenToDatabase(
116+
paymentTokenResponse: PaymentTokenResponse,
117+
) {
118+
// example function to teach saving the paymentToken to a database
119+
// to be used for future transactions
120+
return Promise.resolve();
121+
}
122+
77123
const port = process.env.PORT ?? 8080;
78124

79125
app.listen(port, () => {

0 commit comments

Comments
 (0)