Skip to content

Commit d84f10c

Browse files
authored
Align standard and advanced integrations (#66)
* Minor tweaks to standard integration * Share JS code between card fields and buttons * PR feedback - check for card decline use case
1 parent 5a9a931 commit d84f10c

File tree

7 files changed

+171
-149
lines changed

7 files changed

+171
-149
lines changed

advanced-integration/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Advanced Integration Example
22

3+
This folder contains example code for an Advanced PayPal integration using both the JS SDK and Node.js to complete transactions with the PayPal REST API.
4+
35
## Instructions
46

57
1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`.
68
2. Run `npm install`
79
3. Run `npm start`
810
4. Open http://localhost:8888
9-
5. Enter the credit card number provided from one of your [sandbox accounts](https://developer.paypal.com/dashboard/accounts) or [generate a new credit card](https://developer.paypal.com/dashboard/creditCardGenerator)
11+
5. Enter the credit card number provided from one of your [sandbox accounts](https://developer.paypal.com/dashboard/accounts) or [generate a new credit card](https://developer.paypal.com/dashboard/creditCardGenerator)

advanced-integration/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
{
22
"name": "paypal-advanced-integration",
3+
"description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments",
34
"version": "1.0.0",
4-
"description": "",
55
"main": "server.js",
66
"type": "module",
77
"scripts": {
88
"test": "echo \"Error: no test specified\" && exit 1",
99
"start": "node server.js"
1010
},
11-
"author": "",
1211
"license": "Apache-2.0",
1312
"dependencies": {
1413
"dotenv": "^16.3.1",

advanced-integration/public/app.js

Lines changed: 141 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,192 @@
1+
async function createOrderCallback() {
2+
try {
3+
const response = await fetch('/api/orders', {
4+
method: 'POST',
5+
headers: {
6+
'Content-Type': 'application/json',
7+
},
8+
// use the "body" param to optionally pass additional order information
9+
// like product ids and quantities
10+
body: JSON.stringify({
11+
cart: [
12+
{
13+
id: 'YOUR_PRODUCT_ID',
14+
quantity: 'YOUR_PRODUCT_QUANTITY',
15+
},
16+
],
17+
}),
18+
});
19+
20+
const orderData = await response.json();
21+
22+
if (orderData.id) {
23+
return orderData.id;
24+
} else {
25+
const errorDetail = orderData?.details?.[0];
26+
const errorMessage = errorDetail
27+
? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})`
28+
: JSON.stringify(orderData);
29+
30+
throw new Error(errorMessage);
31+
}
32+
} catch (error) {
33+
console.error(error);
34+
resultMessage(`Could not initiate PayPal Checkout...<br><br>${error}`);
35+
}
36+
}
37+
38+
async function onApproveCallback(data, actions) {
39+
try {
40+
const response = await fetch(`/api/orders/${data.orderID}/capture`, {
41+
method: 'POST',
42+
headers: {
43+
'Content-Type': 'application/json',
44+
},
45+
});
46+
47+
const orderData = await response.json();
48+
// Three cases to handle:
49+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
50+
// (2) Other non-recoverable errors -> Show a failure message
51+
// (3) Successful transaction -> Show confirmation or thank you message
52+
53+
const transaction =
54+
orderData?.purchase_units?.[0]?.payments?.captures?.[0] ||
55+
orderData?.purchase_units?.[0]?.payments?.authorizations?.[0];
56+
const errorDetail = orderData?.details?.[0];
57+
58+
const isHostedFieldsComponent = typeof data.card === 'object';
59+
60+
// this actions.restart() behavior only applies to the Buttons component
61+
if (
62+
errorDetail?.issue === 'INSTRUMENT_DECLINED' &&
63+
isHostedFieldsComponent === false
64+
) {
65+
// (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
66+
// recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/
67+
return actions.restart();
68+
} else if (
69+
errorDetail ||
70+
!transaction ||
71+
transaction.status === 'DECLINED'
72+
) {
73+
// (2) Other non-recoverable errors -> Show a failure message
74+
let errorMessage;
75+
if (transaction) {
76+
errorMessage = `Transaction ${transaction.status}: ${transaction.id}`;
77+
} else if (errorDetail) {
78+
errorMessage = `${errorDetail.description} (${orderData.debug_id})`;
79+
} else {
80+
errorMessage = JSON.stringify(orderData);
81+
}
82+
83+
throw new Error(errorMessage);
84+
} else {
85+
// (3) Successful transaction -> Show confirmation or thank you message
86+
// Or go to another URL: actions.redirect('thank_you.html');
87+
resultMessage(
88+
`Transaction ${transaction.status}: ${transaction.id}<br><br>See console for all available details`,
89+
);
90+
console.log(
91+
'Capture result',
92+
orderData,
93+
JSON.stringify(orderData, null, 2),
94+
);
95+
}
96+
} catch (error) {
97+
console.error(error);
98+
resultMessage(
99+
`Sorry, your transaction could not be processed...<br><br>${error}`,
100+
);
101+
}
102+
}
103+
1104
paypal
2105
.Buttons({
3-
// Sets up the transaction when a payment button is clicked
4-
createOrder: function () {
5-
return fetch("/api/orders", {
6-
method: "POST",
7-
headers: {
8-
'Content-Type': 'application/json',
9-
},
10-
// use the "body" param to optionally pass additional order information
11-
// like product skus and quantities
12-
body: JSON.stringify({
13-
cart: [
14-
{
15-
sku: "<YOUR_PRODUCT_STOCK_KEEPING_UNIT>",
16-
quantity: "<YOUR_PRODUCT_QUANTITY>",
17-
},
18-
],
19-
}),
20-
})
21-
.then((response) => response.json())
22-
.then((order) => order.id);
23-
},
24-
// Finalize the transaction after payer approval
25-
onApprove: function (data) {
26-
return fetch(`/api/orders/${data.orderID}/capture`, {
27-
method: "POST",
28-
headers: {
29-
'Content-Type': 'application/json',
30-
},
31-
})
32-
.then((response) => response.json())
33-
.then((orderData) => {
34-
// Successful capture! For dev/demo purposes:
35-
console.log(
36-
"Capture result",
37-
orderData,
38-
JSON.stringify(orderData, null, 2)
39-
);
40-
const transaction = orderData.purchase_units[0].payments.captures[0];
41-
alert(`Transaction ${transaction.status}: ${transaction.id}
42-
43-
See console for all available details
44-
`);
45-
// When ready to go live, remove the alert and show a success message within this page. For example:
46-
// var element = document.getElementById('paypal-button-container');
47-
// element.innerHTML = '<h3>Thank you for your payment!</h3>';
48-
// Or go to another URL: actions.redirect('thank_you.html');
49-
});
50-
},
106+
createOrder: createOrderCallback,
107+
onApprove: onApproveCallback,
51108
})
52-
.render("#paypal-button-container");
109+
.render('#paypal-button-container');
110+
111+
// Example function to show a result to the user. Your site's UI library can be used instead.
112+
function resultMessage(message) {
113+
const container = document.querySelector('#result-message');
114+
container.innerHTML = message;
115+
}
53116

54117
// If this returns false or the card fields aren't visible, see Step #1.
55118
if (paypal.HostedFields.isEligible()) {
56-
let orderId;
57-
58119
// Renders card fields
59120
paypal.HostedFields.render({
60121
// Call your server to set up the transaction
61-
createOrder: () => {
62-
return fetch("/api/orders", {
63-
method: "POST",
64-
headers: {
65-
'Content-Type': 'application/json',
66-
},
67-
// use the "body" param to optionally pass additional order information
68-
// like product skus and quantities
69-
body: JSON.stringify({
70-
cart: [
71-
{
72-
sku: "<YOUR_PRODUCT_STOCK_KEEPING_UNIT>",
73-
quantity: "<YOUR_PRODUCT_QUANTITY>",
74-
},
75-
],
76-
}),
77-
})
78-
.then((res) => res.json())
79-
.then((orderData) => {
80-
orderId = orderData.id; // needed later to complete capture
81-
return orderData.id;
82-
});
83-
},
122+
createOrder: createOrderCallback,
84123
styles: {
85-
".valid": {
86-
color: "green",
124+
'.valid': {
125+
color: 'green',
87126
},
88-
".invalid": {
89-
color: "red",
127+
'.invalid': {
128+
color: 'red',
90129
},
91130
},
92131
fields: {
93132
number: {
94-
selector: "#card-number",
95-
placeholder: "4111 1111 1111 1111",
133+
selector: '#card-number',
134+
placeholder: '4111 1111 1111 1111',
96135
},
97136
cvv: {
98-
selector: "#cvv",
99-
placeholder: "123",
137+
selector: '#cvv',
138+
placeholder: '123',
100139
},
101140
expirationDate: {
102-
selector: "#expiration-date",
103-
placeholder: "MM/YY",
141+
selector: '#expiration-date',
142+
placeholder: 'MM/YY',
104143
},
105144
},
106145
}).then((cardFields) => {
107-
document.querySelector("#card-form").addEventListener("submit", (event) => {
146+
document.querySelector('#card-form').addEventListener('submit', (event) => {
108147
event.preventDefault();
109148
cardFields
110149
.submit({
111150
// Cardholder's first and last name
112-
cardholderName: document.getElementById("card-holder-name").value,
151+
cardholderName: document.getElementById('card-holder-name').value,
113152
// Billing Address
114153
billingAddress: {
115154
// Street address, line 1
116155
streetAddress: document.getElementById(
117-
"card-billing-address-street"
156+
'card-billing-address-street',
118157
).value,
119158
// Street address, line 2 (Ex: Unit, Apartment, etc.)
120159
extendedAddress: document.getElementById(
121-
"card-billing-address-unit"
160+
'card-billing-address-unit',
122161
).value,
123162
// State
124-
region: document.getElementById("card-billing-address-state").value,
163+
region: document.getElementById('card-billing-address-state').value,
125164
// City
126-
locality: document.getElementById("card-billing-address-city")
165+
locality: document.getElementById('card-billing-address-city')
127166
.value,
128167
// Postal Code
129-
postalCode: document.getElementById("card-billing-address-zip")
168+
postalCode: document.getElementById('card-billing-address-zip')
130169
.value,
131170
// Country Code
132171
countryCodeAlpha2: document.getElementById(
133-
"card-billing-address-country"
172+
'card-billing-address-country',
134173
).value,
135174
},
136175
})
137-
.then(() => {
138-
fetch(`/api/orders/${orderId}/capture`, {
139-
method: "POST",
140-
headers: {
141-
'Content-Type': 'application/json',
142-
},
143-
})
144-
.then((res) => res.json())
145-
.then((orderData) => {
146-
// Two cases to handle:
147-
// (1) Other non-recoverable errors -> Show a failure message
148-
// (2) Successful transaction -> Show confirmation or thank you
149-
// This example reads a v2/checkout/orders capture response, propagated from the server
150-
// You could use a different API or structure for your 'orderData'
151-
const errorDetail =
152-
Array.isArray(orderData.details) && orderData.details[0];
153-
if (errorDetail) {
154-
var msg = "Sorry, your transaction could not be processed.";
155-
if (errorDetail.description)
156-
msg += "\n\n" + errorDetail.description;
157-
if (orderData.debug_id) msg += " (" + orderData.debug_id + ")";
158-
return alert(msg); // Show a failure message
159-
}
160-
// Show a success message or redirect
161-
alert("Transaction completed!");
162-
});
176+
.then((data) => {
177+
return onApproveCallback(data);
163178
})
164-
.catch((err) => {
165-
alert("Payment could not be captured! " + JSON.stringify(err));
179+
.catch((orderData) => {
180+
const { links, ...errorMessageData } = orderData;
181+
resultMessage(
182+
`Sorry, your transaction could not be processed...<br><br>${JSON.stringify(
183+
errorMessageData,
184+
)}`,
185+
);
166186
});
167187
});
168188
});
169189
} else {
170190
// Hides card fields if the merchant isn't eligible
171-
document.querySelector("#card-form").style = "display: none";
191+
document.querySelector('#card-form').style = 'display: none';
172192
}

0 commit comments

Comments
 (0)