feat(donate): PayPal integration
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
e3db423abf
commit
6c6eadfbe4
61
api-server/package-lock.json
generated
61
api-server/package-lock.json
generated
@ -2302,21 +2302,11 @@
|
|||||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.19.0",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||||
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
|
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "1.5.10",
|
"follow-redirects": "1.5.10"
|
||||||
"is-buffer": "^2.0.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"is-buffer": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"babel-core": {
|
"babel-core": {
|
||||||
@ -4707,7 +4697,6 @@
|
|||||||
"version": "1.5.10",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "=3.1.0"
|
"debug": "=3.1.0"
|
||||||
},
|
},
|
||||||
@ -4716,7 +4705,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
@ -4804,8 +4792,7 @@
|
|||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
@ -4826,14 +4813,12 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@ -4848,20 +4833,17 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -4978,8 +4960,7 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@ -4991,7 +4972,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -5006,7 +4986,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@ -5014,14 +4993,12 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@ -5040,7 +5017,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@ -5121,8 +5097,7 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@ -5134,7 +5109,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@ -5220,8 +5194,7 @@
|
|||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -5257,7 +5230,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@ -5277,7 +5249,6 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
@ -5321,14 +5292,12 @@
|
|||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"@freecodecamp/loopback-component-passport": "^1.1.0",
|
"@freecodecamp/loopback-component-passport": "^1.1.0",
|
||||||
"accepts": "^1.3.7",
|
"accepts": "^1.3.7",
|
||||||
"auth0-js": "^9.11.3",
|
"auth0-js": "^9.11.3",
|
||||||
|
"axios": "^0.19.2",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"chai": "~3.4.1",
|
"chai": "~3.4.1",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
|
@ -3,15 +3,27 @@ import debug from 'debug';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { isEmail, isNumeric } from 'validator';
|
import { isEmail, isNumeric } from 'validator';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAsyncPaypalToken,
|
||||||
|
verifyWebHook,
|
||||||
|
updateUser,
|
||||||
|
verifyWebHookType
|
||||||
|
} from '../utils/donation';
|
||||||
import {
|
import {
|
||||||
durationKeysConfig,
|
durationKeysConfig,
|
||||||
donationOneTimeConfig,
|
donationOneTimeConfig,
|
||||||
donationSubscriptionConfig
|
donationSubscriptionConfig,
|
||||||
|
paypalConfig
|
||||||
} from '../../../config/donation-settings';
|
} from '../../../config/donation-settings';
|
||||||
import keys from '../../../config/secrets';
|
import keys from '../../../config/secrets';
|
||||||
|
|
||||||
const log = debug('fcc:boot:donate');
|
const log = debug('fcc:boot:donate');
|
||||||
|
|
||||||
|
const paypalWebhookId =
|
||||||
|
process.env.FREECODECAMP_NODE_ENV === 'production'
|
||||||
|
? paypalConfig.production.webhookId
|
||||||
|
: paypalConfig.development.webhookId;
|
||||||
|
|
||||||
export default function donateBoot(app, done) {
|
export default function donateBoot(app, done) {
|
||||||
let stripe = false;
|
let stripe = false;
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
@ -243,27 +255,92 @@ export default function donateBoot(app, done) {
|
|||||||
.send({ error: 'Donation failed due to a server error.' })
|
.send({ error: 'Donation failed due to a server error.' })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function addDonation(req, res) {
|
||||||
|
const { user, body } = req;
|
||||||
|
|
||||||
const pubKey = keys.stripe.public;
|
if (!user || !body) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.send({ error: 'User must be signed in for this request.' });
|
||||||
|
}
|
||||||
|
return Promise.resolve(req)
|
||||||
|
.then(
|
||||||
|
user.updateAttributes({
|
||||||
|
isDonating: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => res.status(200).json({ isDonating: true }))
|
||||||
|
.catch(err => {
|
||||||
|
log(err.message);
|
||||||
|
return res.status(500).send({
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Something went wrong.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePaypal(req, res) {
|
||||||
|
const { headers, body } = req;
|
||||||
|
return Promise.resolve(req)
|
||||||
|
.then(verifyWebHookType)
|
||||||
|
.then(getAsyncPaypalToken)
|
||||||
|
.then(token => verifyWebHook(headers, body, token, paypalWebhookId))
|
||||||
|
.then(hookBody => updateUser(hookBody, app))
|
||||||
|
.then(() => res.status(200).json({ message: 'received hook' }))
|
||||||
|
.catch(err => {
|
||||||
|
log(err.message);
|
||||||
|
return res.status(200).json({ message: 'received hook' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripeKey = keys.stripe.public;
|
||||||
const secKey = keys.stripe.secret;
|
const secKey = keys.stripe.secret;
|
||||||
|
const paypalKey = keys.paypal.client;
|
||||||
|
const paypalSec = keys.paypal.secret;
|
||||||
const hmacKey = keys.servicebot.hmacKey;
|
const hmacKey = keys.servicebot.hmacKey;
|
||||||
const secretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
|
const stripeSecretInvalid = !secKey || secKey === 'sk_from_stripe_dashboard';
|
||||||
const publicInvalid = !pubKey || pubKey === 'pk_from_stripe_dashboard';
|
const stripPublicInvalid =
|
||||||
|
!stripeKey || stripeKey === 'pk_from_stripe_dashboard';
|
||||||
|
|
||||||
|
const paypalSecretInvalid =
|
||||||
|
!paypalKey || paypalKey === 'id_from_paypal_dashboard';
|
||||||
|
const paypalPublicInvalid =
|
||||||
|
!paypalSec || paypalSec === 'secret_from_paypal_dashboard';
|
||||||
const hmacKeyInvalid =
|
const hmacKeyInvalid =
|
||||||
!hmacKey || hmacKey === 'secret_key_from_servicebot_dashboard';
|
!hmacKey || hmacKey === 'secret_key_from_servicebot_dashboard';
|
||||||
|
const paypalInvalid = paypalPublicInvalid || paypalSecretInvalid;
|
||||||
if (secretInvalid || publicInvalid || hmacKeyInvalid) {
|
const stripeInvalid = stripeSecretInvalid || stripPublicInvalid;
|
||||||
|
if (stripeInvalid) {
|
||||||
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
throw new Error('Stripe API keys are required to boot the server!');
|
throw new Error('Stripe API keys are required to boot the server!');
|
||||||
}
|
}
|
||||||
console.info('No Stripe API keys were found, moving on...');
|
console.info('No Stripe API keys were found, moving on...');
|
||||||
done();
|
|
||||||
} else {
|
} else {
|
||||||
api.post('/charge-stripe', createStripeDonation);
|
api.post('/charge-stripe', createStripeDonation);
|
||||||
api.post('/create-hmac-hash', createHmacHash);
|
api.post('/create-hmac-hash', createHmacHash);
|
||||||
donateRouter.use('/donate', api);
|
}
|
||||||
app.use(donateRouter);
|
if (paypalInvalid) {
|
||||||
app.use('/internal', donateRouter);
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
|
throw new Error('PayPal API keys are required to boot the server!');
|
||||||
|
}
|
||||||
|
console.info('No PayPal API keys were found, moving on...');
|
||||||
|
} else {
|
||||||
|
api.post('/update-paypal', updatePaypal);
|
||||||
|
api.post('/add-donation', addDonation);
|
||||||
|
}
|
||||||
|
if (hmacKeyInvalid) {
|
||||||
|
if (process.env.FREECODECAMP_NODE_ENV === 'production') {
|
||||||
|
throw new Error('Servicebot HMAC key is required to boot the server!');
|
||||||
|
}
|
||||||
|
console.info('No servicebot HMAC key was found, moving on...');
|
||||||
|
}
|
||||||
|
donateRouter.use('/donate', api);
|
||||||
|
app.use(donateRouter);
|
||||||
|
app.use('/internal', donateRouter);
|
||||||
|
|
||||||
|
if (stripeInvalid) {
|
||||||
|
done();
|
||||||
|
} else {
|
||||||
connectToStripe().then(done);
|
connectToStripe().then(done);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
/* global jest*/
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { isEmail } from 'validator';
|
||||||
|
|
||||||
export const firstChallengeUrl = '/learn/the/first/challenge';
|
export const firstChallengeUrl = '/learn/the/first/challenge';
|
||||||
export const requestedChallengeUrl = '/learn/my/actual/challenge';
|
export const requestedChallengeUrl = '/learn/my/actual/challenge';
|
||||||
|
|
||||||
@ -62,15 +66,55 @@ export const mockCompletedChallenges = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
export const mockUserID = '5c7d892aff9777c8b1c1a95e';
|
export const mockUserID = '5c7d892aff9777c8b1c1a95e';
|
||||||
|
|
||||||
|
export const createUserMockFn = jest.fn();
|
||||||
|
export const createDonationMockFn = jest.fn();
|
||||||
|
export const updateDonationAttr = jest.fn();
|
||||||
|
export const updateUserAttr = jest.fn();
|
||||||
export const mockUser = {
|
export const mockUser = {
|
||||||
id: mockUserID,
|
id: mockUserID,
|
||||||
username: 'camperbot',
|
username: 'camperbot',
|
||||||
currentChallengeId: '123abc',
|
currentChallengeId: '123abc',
|
||||||
|
email: 'donor@freecodecamp.com',
|
||||||
timezone: 'UTC',
|
timezone: 'UTC',
|
||||||
completedChallenges: mockCompletedChallenges,
|
completedChallenges: mockCompletedChallenges,
|
||||||
progressTimestamps: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
progressTimestamps: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
|
isDonating: true,
|
||||||
|
donationEmails: ['donor@freecodecamp.com', 'donor@freecodecamp.com'],
|
||||||
|
createDonation: donation => {
|
||||||
|
createDonationMockFn(donation);
|
||||||
|
return mockObservable;
|
||||||
|
},
|
||||||
|
updateAttributes: updateUserAttr
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockObservable = {
|
||||||
|
toPromise: () => Promise.resolve('result')
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockDonation = {
|
||||||
|
id: '5e5f8eda5ed7be2b54e18718',
|
||||||
|
email: 'donor@freecodecamp.com',
|
||||||
|
provider: 'paypal',
|
||||||
|
amount: 500,
|
||||||
|
duration: 'month',
|
||||||
|
startDate: {
|
||||||
|
_when: '2018-11-01T00:00:00.000Z',
|
||||||
|
_date: '2018-11-01T00:00:00.000Z'
|
||||||
|
},
|
||||||
|
subscriptionId: 'I-BA1ATBNF8T3P',
|
||||||
|
userId: mockUserID,
|
||||||
|
updateAttributes: updateDonationAttr
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createNewUserFromEmail(email) {
|
||||||
|
const newMockUser = mockUser;
|
||||||
|
newMockUser.email = email;
|
||||||
|
newMockUser.username = 'camberbot2';
|
||||||
|
newMockUser.ID = '5c7d892aff9888c8b1c1a95e';
|
||||||
|
return newMockUser;
|
||||||
|
}
|
||||||
|
|
||||||
export const mockApp = {
|
export const mockApp = {
|
||||||
models: {
|
models: {
|
||||||
Challenge: {
|
Challenge: {
|
||||||
@ -83,12 +127,31 @@ export const mockApp = {
|
|||||||
: cb(new Error('challenge not found'));
|
: cb(new Error('challenge not found'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Donation: {
|
||||||
|
findOne(query, cb) {
|
||||||
|
return isEqual(query, matchSubscriptionIdQuery)
|
||||||
|
? cb(null, mockDonation)
|
||||||
|
: cb(Error('No Donation'));
|
||||||
|
}
|
||||||
|
},
|
||||||
User: {
|
User: {
|
||||||
findById(id, cb) {
|
findById(id, cb) {
|
||||||
if (id === mockUser.id) {
|
if (id === mockUser.id) {
|
||||||
return cb(null, mockUser);
|
return cb(null, mockUser);
|
||||||
}
|
}
|
||||||
return cb(Error('No user'));
|
return cb(Error('No user'));
|
||||||
|
},
|
||||||
|
findOne(query, cb) {
|
||||||
|
if (isEqual(query, matchEmailQuery) || isEqual(query, matchUserIdQuery))
|
||||||
|
return cb(null, mockUser);
|
||||||
|
return cb(null, null);
|
||||||
|
},
|
||||||
|
create(query, cb) {
|
||||||
|
if (!isEmail(query.email)) return cb(new Error('email not valid'));
|
||||||
|
else if (query.email === mockUser.email)
|
||||||
|
return cb(new Error('user exist'));
|
||||||
|
createUserMockFn();
|
||||||
|
return Promise.resolve(createNewUserFromEmail(query.email));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,6 +159,16 @@ export const mockApp = {
|
|||||||
|
|
||||||
export const mockGetFirstChallenge = () => firstChallengeUrl;
|
export const mockGetFirstChallenge = () => firstChallengeUrl;
|
||||||
|
|
||||||
|
export const matchEmailQuery = {
|
||||||
|
where: { email: mockUser.email }
|
||||||
|
};
|
||||||
|
export const matchSubscriptionIdQuery = {
|
||||||
|
where: { subscriptionId: mockDonation.subscriptionId }
|
||||||
|
};
|
||||||
|
export const matchUserIdQuery = {
|
||||||
|
where: { id: mockUser.id }
|
||||||
|
};
|
||||||
|
|
||||||
export const firstChallengeQuery = {
|
export const firstChallengeQuery = {
|
||||||
// first challenge of the first block of the first superBlock
|
// first challenge of the first block of the first superBlock
|
||||||
where: { challengeOrder: 0, superOrder: 1, order: 0 }
|
where: { challengeOrder: 0, superOrder: 1, order: 0 }
|
||||||
|
@ -6,6 +6,7 @@ export default function() {
|
|||||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Note: paypal webhook goes through /internal
|
||||||
return function csrf(req, res, next) {
|
return function csrf(req, res, next) {
|
||||||
const path = req.path.split('/')[1];
|
const path = req.path.split('/')[1];
|
||||||
if (/(^api$|^unauthenticated$|^internal$|^p$)/.test(path)) {
|
if (/(^api$|^unauthenticated$|^internal$|^p$)/.test(path)) {
|
||||||
|
@ -17,8 +17,14 @@ const apiProxyRE = /^\/internal\/|^\/external\//;
|
|||||||
const newsShortLinksRE = /^\/internal\/n\/|^\/internal\/p\?/;
|
const newsShortLinksRE = /^\/internal\/n\/|^\/internal\/p\?/;
|
||||||
const loopbackAPIPathRE = /^\/internal\/api\//;
|
const loopbackAPIPathRE = /^\/internal\/api\//;
|
||||||
const showCertRe = /^\/internal\/certificate\/showCert\//;
|
const showCertRe = /^\/internal\/certificate\/showCert\//;
|
||||||
|
const updatePaypalRe = /^\/internal\/donate\/update-paypal/;
|
||||||
|
|
||||||
const _whiteListREs = [newsShortLinksRE, loopbackAPIPathRE, showCertRe];
|
const _whiteListREs = [
|
||||||
|
newsShortLinksRE,
|
||||||
|
loopbackAPIPathRE,
|
||||||
|
showCertRe,
|
||||||
|
updatePaypalRe
|
||||||
|
];
|
||||||
|
|
||||||
export function isWhiteListedPath(path, whiteListREs = _whiteListREs) {
|
export function isWhiteListedPath(path, whiteListREs = _whiteListREs) {
|
||||||
return whiteListREs.some(re => re.test(path));
|
return whiteListREs.some(re => re.test(path));
|
||||||
|
214
api-server/server/utils/__mocks__/donation.js
Normal file
214
api-server/server/utils/__mocks__/donation.js
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
export const mockCancellationHook = {
|
||||||
|
headers: {
|
||||||
|
host: 'a47fb0f4.ngrok.io',
|
||||||
|
accept: '*/*',
|
||||||
|
'paypal-transmission-id': '2e24bc40-61d1-11ea-8ac4-7d4e2605c70c',
|
||||||
|
'paypal-transmission-time': '2020-03-09T06:42:43Z',
|
||||||
|
'paypal-transmission-sig': 'ODCa4gXmfnxkNga1t9p2HTIWFjlTj68P7MhueQd',
|
||||||
|
'paypal-auth-version': 'v2',
|
||||||
|
'paypal-cert-url': 'https://api.sandbox.paypal.com/v1/notifications/certs',
|
||||||
|
'paypal-auth-algo': 'SHA256withRSA',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user-agent': 'PayPal/AUHD-214.0-54280748',
|
||||||
|
'correlation-id': 'c3823d4c07ce5',
|
||||||
|
cal_poolstack: 'amqunphttpdeliveryd:UNPHTTPDELIVERY',
|
||||||
|
client_pid: '23853',
|
||||||
|
'content-length': '1706',
|
||||||
|
'x-forwarded-proto': 'https',
|
||||||
|
'x-forwarded-for': '173.0.82.126'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: 'WH-1VF24938EU372274X-83540367M0110254R',
|
||||||
|
event_version: '1.0',
|
||||||
|
create_time: '2020-03-06T15:34:50.000Z',
|
||||||
|
resource_type: 'subscription',
|
||||||
|
resource_version: '2.0',
|
||||||
|
event_type: 'BILLING.SUBSCRIPTION.CANCELLED',
|
||||||
|
summary: 'Subscription cancelled',
|
||||||
|
resource: {
|
||||||
|
shipping_amount: { currency_code: 'USD', value: '0.0' },
|
||||||
|
start_time: '2020-03-05T08:00:00Z',
|
||||||
|
update_time: '2020-03-09T06:42:09Z',
|
||||||
|
quantity: '1',
|
||||||
|
subscriber: {
|
||||||
|
name: [Object],
|
||||||
|
email_address: 'sb-zdry81054163@personal.example.com',
|
||||||
|
payer_id: '82PVXVLDAU3E8',
|
||||||
|
shipping_address: [Object]
|
||||||
|
},
|
||||||
|
billing_info: {
|
||||||
|
outstanding_balance: [Object],
|
||||||
|
cycle_executions: [Array],
|
||||||
|
last_payment: [Object],
|
||||||
|
next_billing_time: '2020-04-05T10:00:00Z',
|
||||||
|
failed_payments_count: 0
|
||||||
|
},
|
||||||
|
create_time: '2020-03-06T07:34:50Z',
|
||||||
|
links: [[Object]],
|
||||||
|
id: 'I-BA1ATBNF8T3P',
|
||||||
|
plan_id: 'P-6VP46874PR423771HLZDKFBA',
|
||||||
|
status: 'CANCELLED',
|
||||||
|
status_update_time: '2020-03-09T06:42:09Z'
|
||||||
|
},
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R',
|
||||||
|
rel: 'self',
|
||||||
|
method: 'GET'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1VF24938EU372274X-83540367M0110254R/resend',
|
||||||
|
rel: 'resend',
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const mockActivationHook = {
|
||||||
|
headers: {
|
||||||
|
host: 'a47fb0f4.ngrok.io',
|
||||||
|
accept: '*/*',
|
||||||
|
'paypal-transmission-id': '22103660-5f7d-11ea-8ac4-7d4e2605c70c',
|
||||||
|
'paypal-transmission-time': '2020-03-06T07:36:03Z',
|
||||||
|
'paypal-transmission-sig':
|
||||||
|
'a;sldfn;lqwjhepjtn12l3n5123mnpu1i-sc-_+++dsflqenwpk1n234uthmsqwr123',
|
||||||
|
'paypal-auth-version': 'v2',
|
||||||
|
'paypal-cert-url':
|
||||||
|
'https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270',
|
||||||
|
'paypal-auth-algo': 'SHASHASHA',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user-agent': 'PayPal/AUHD-214.0-54280748',
|
||||||
|
'correlation-id': 'e0b25772e11af',
|
||||||
|
client_pid: '14973',
|
||||||
|
'content-length': '2201',
|
||||||
|
'x-forwarded-proto': 'https',
|
||||||
|
'x-forwarded-for': '173.0.82.126'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
id: 'WH-77687562XN25889J8-8Y6T55435R66168T6',
|
||||||
|
create_time: '2018-19-12T22:20:32.000Z',
|
||||||
|
resource_type: 'subscription',
|
||||||
|
event_type: 'BILLING.SUBSCRIPTION.ACTIVATED',
|
||||||
|
summary: 'A billing agreement was activated.',
|
||||||
|
resource: {
|
||||||
|
quantity: '20',
|
||||||
|
subscriber: {
|
||||||
|
name: {
|
||||||
|
given_name: 'John',
|
||||||
|
surname: 'Doe'
|
||||||
|
},
|
||||||
|
email_address: 'donor@freecodecamp.com',
|
||||||
|
shipping_address: {
|
||||||
|
name: {
|
||||||
|
full_name: 'John Doe'
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
address_line_1: '2211 N First Street',
|
||||||
|
address_line_2: 'Building 17',
|
||||||
|
admin_area_2: 'San Jose',
|
||||||
|
admin_area_1: 'CA',
|
||||||
|
postal_code: '95131',
|
||||||
|
country_code: 'US'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create_time: '2018-12-10T21:20:49Z',
|
||||||
|
shipping_amount: {
|
||||||
|
currency_code: 'USD',
|
||||||
|
value: '10.00'
|
||||||
|
},
|
||||||
|
start_time: '2018-11-01T00:00:00Z',
|
||||||
|
update_time: '2018-12-10T21:20:49Z',
|
||||||
|
billing_info: {
|
||||||
|
outstanding_balance: {
|
||||||
|
currency_code: 'USD',
|
||||||
|
value: '10.00'
|
||||||
|
},
|
||||||
|
cycle_executions: [
|
||||||
|
{
|
||||||
|
tenure_type: 'TRIAL',
|
||||||
|
sequence: 1,
|
||||||
|
cycles_completed: 1,
|
||||||
|
cycles_remaining: 0,
|
||||||
|
current_pricing_scheme_version: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tenure_type: 'REGULAR',
|
||||||
|
sequence: 2,
|
||||||
|
cycles_completed: 1,
|
||||||
|
cycles_remaining: 0,
|
||||||
|
current_pricing_scheme_version: 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
last_payment: {
|
||||||
|
amount: {
|
||||||
|
currency_code: 'USD',
|
||||||
|
value: '500.00'
|
||||||
|
},
|
||||||
|
time: '2018-12-01T01:20:49Z'
|
||||||
|
},
|
||||||
|
next_billing_time: '2019-01-01T00:20:49Z',
|
||||||
|
final_payment_time: '2020-01-01T00:20:49Z',
|
||||||
|
failed_payments_count: 2
|
||||||
|
},
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
|
||||||
|
rel: 'self',
|
||||||
|
method: 'GET'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G',
|
||||||
|
rel: 'edit',
|
||||||
|
method: 'PATCH'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/suspend',
|
||||||
|
rel: 'suspend',
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/cancel',
|
||||||
|
rel: 'cancel',
|
||||||
|
method: 'POST'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G/capture',
|
||||||
|
rel: 'capture',
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
id: 'I-BW452GLLEP1G',
|
||||||
|
plan_id: 'P-5ML4271244454362WXNWU5NQ',
|
||||||
|
auto_renewal: true,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
status_update_time: '2018-12-10T21:20:49Z'
|
||||||
|
},
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6',
|
||||||
|
rel: 'self',
|
||||||
|
method: 'GET',
|
||||||
|
encType: 'application/json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href:
|
||||||
|
'https://api.paypal.com/v1/notifications/webhooks-events/WH-77687562XN25889J8-8Y6T55435R66168T6/resend',
|
||||||
|
rel: 'resend',
|
||||||
|
method: 'POST',
|
||||||
|
encType: 'application/json'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
event_version: '1.0',
|
||||||
|
resource_version: '2.0'
|
||||||
|
}
|
||||||
|
};
|
169
api-server/server/utils/donation-uttils.test.js
Normal file
169
api-server/server/utils/donation-uttils.test.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
/* global describe it expect */
|
||||||
|
/* global jest*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import keys from '../../../config/secrets';
|
||||||
|
import {
|
||||||
|
getAsyncPaypalToken,
|
||||||
|
verifyWebHook,
|
||||||
|
updateUser,
|
||||||
|
capitalizeKeys,
|
||||||
|
createDonationObj
|
||||||
|
} from './donation';
|
||||||
|
import { mockActivationHook, mockCancellationHook } from './__mocks__/donation';
|
||||||
|
import {
|
||||||
|
mockApp,
|
||||||
|
createDonationMockFn,
|
||||||
|
createUserMockFn,
|
||||||
|
updateDonationAttr,
|
||||||
|
updateUserAttr
|
||||||
|
} from '../boot_tests/fixtures';
|
||||||
|
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
const sandBoxSubdomain =
|
||||||
|
process.env.FREECODECAMP_NODE_ENV === 'production' ? '' : 'sandbox.';
|
||||||
|
|
||||||
|
const verificationUrl = `https://api.${sandBoxSubdomain}paypal.com/v1/notifications/verify-webhook-signature`;
|
||||||
|
const tokenUrl = `https://api.${sandBoxSubdomain}paypal.com/v1/oauth2/token`;
|
||||||
|
const {
|
||||||
|
body: activationHookBody,
|
||||||
|
headers: activationHookHeaders
|
||||||
|
} = mockActivationHook;
|
||||||
|
|
||||||
|
describe('donation', () => {
|
||||||
|
describe('getAsyncPaypalToken', () => {
|
||||||
|
it('call paypal api for token ', async () => {
|
||||||
|
const res = {
|
||||||
|
data: {
|
||||||
|
access_token: 'token'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.post.mockImplementationOnce(() => Promise.resolve(res));
|
||||||
|
|
||||||
|
await expect(getAsyncPaypalToken()).resolves.toEqual(
|
||||||
|
res.data.access_token
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledTimes(1);
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(tokenUrl, null, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
username: keys.paypal.client,
|
||||||
|
password: keys.paypal.secret
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
grant_type: 'client_credentials'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verifyWebHook', () => {
|
||||||
|
// normalize headers
|
||||||
|
capitalizeKeys(activationHookHeaders);
|
||||||
|
const mockWebhookId = 'qwdfq;3w12341dfa4';
|
||||||
|
const mockAccessToken = '241231223$!@$#1243';
|
||||||
|
const mockPayLoad = {
|
||||||
|
auth_algo: activationHookHeaders['PAYPAL-AUTH-ALGO'],
|
||||||
|
cert_url: activationHookHeaders['PAYPAL-CERT-URL'],
|
||||||
|
transmission_id: activationHookHeaders['PAYPAL-TRANSMISSION-ID'],
|
||||||
|
transmission_sig: activationHookHeaders['PAYPAL-TRANSMISSION-SIG'],
|
||||||
|
transmission_time: activationHookHeaders['PAYPAL-TRANSMISSION-TIME'],
|
||||||
|
webhook_id: mockWebhookId,
|
||||||
|
webhook_event: activationHookBody
|
||||||
|
};
|
||||||
|
const failedVerificationErr = {
|
||||||
|
message: `Failed token verification.`,
|
||||||
|
type: 'FailedPaypalTokenVerificationError'
|
||||||
|
};
|
||||||
|
const axiosOptions = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${mockAccessToken}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const successVerificationResponce = {
|
||||||
|
data: {
|
||||||
|
verification_status: 'SUCCESS'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const failedVerificationResponce = {
|
||||||
|
data: {
|
||||||
|
verification_status: 'FAILED'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('calls paypal for Webhook verification', async () => {
|
||||||
|
axios.post.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(successVerificationResponce)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
verifyWebHook(
|
||||||
|
activationHookHeaders,
|
||||||
|
activationHookBody,
|
||||||
|
mockAccessToken,
|
||||||
|
mockWebhookId
|
||||||
|
)
|
||||||
|
).resolves.toEqual(activationHookBody);
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
|
verificationUrl,
|
||||||
|
mockPayLoad,
|
||||||
|
axiosOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('throws error if verification not successful', async () => {
|
||||||
|
axios.post.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(failedVerificationResponce)
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
verifyWebHook(
|
||||||
|
activationHookHeaders,
|
||||||
|
activationHookBody,
|
||||||
|
mockAccessToken,
|
||||||
|
mockWebhookId
|
||||||
|
)
|
||||||
|
).rejects.toEqual(failedVerificationErr);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateUser', () => {
|
||||||
|
it('created a donation when a machting user found', () => {
|
||||||
|
updateUser(activationHookBody, mockApp);
|
||||||
|
expect(createDonationMockFn).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createDonationMockFn).toHaveBeenCalledWith(
|
||||||
|
createDonationObj(activationHookBody)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('create a user and donation when no machting user found', () => {
|
||||||
|
let newActivationHookBody = activationHookBody;
|
||||||
|
newActivationHookBody.resource.subscriber.email_address =
|
||||||
|
'new@freecodecamp.org';
|
||||||
|
updateUser(newActivationHookBody, mockApp);
|
||||||
|
expect(createUserMockFn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('modify user and donation records on cancellation', () => {
|
||||||
|
const { body: cancellationHookBody } = mockCancellationHook;
|
||||||
|
const {
|
||||||
|
resource: { status_update_time = new Date(Date.now()).toISOString() }
|
||||||
|
} = cancellationHookBody;
|
||||||
|
|
||||||
|
updateUser(cancellationHookBody, mockApp);
|
||||||
|
expect(updateDonationAttr).toHaveBeenCalledWith({
|
||||||
|
endDate: new Date(status_update_time).toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateUserAttr).toHaveBeenCalledWith({
|
||||||
|
isDonating: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
182
api-server/server/utils/donation.js
Normal file
182
api-server/server/utils/donation.js
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import axios from 'axios';
|
||||||
|
import debug from 'debug';
|
||||||
|
import keys from '../../../config/secrets';
|
||||||
|
|
||||||
|
const log = debug('fcc:boot:donate');
|
||||||
|
|
||||||
|
const sandBoxSubdomain =
|
||||||
|
process.env.FREECODECAMP_NODE_ENV === 'production' ? '' : 'sandbox.';
|
||||||
|
|
||||||
|
const verificationUrl = `https://api.${sandBoxSubdomain}paypal.com/v1/notifications/verify-webhook-signature`;
|
||||||
|
const tokenUrl = `https://api.${sandBoxSubdomain}paypal.com/v1/oauth2/token`;
|
||||||
|
|
||||||
|
export async function getAsyncPaypalToken() {
|
||||||
|
const res = await axios.post(tokenUrl, null, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
username: keys.paypal.client,
|
||||||
|
password: keys.paypal.secret
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
grant_type: 'client_credentials'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res.data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalizeKeys(object) {
|
||||||
|
Object.keys(object).forEach(function(key) {
|
||||||
|
object[key.toUpperCase()] = object[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyWebHook(headers, body, token, webhookId) {
|
||||||
|
var webhookEventBody = typeof body === 'string' ? JSON.parse(body) : body;
|
||||||
|
|
||||||
|
capitalizeKeys(headers);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
auth_algo: headers['PAYPAL-AUTH-ALGO'],
|
||||||
|
cert_url: headers['PAYPAL-CERT-URL'],
|
||||||
|
transmission_id: headers['PAYPAL-TRANSMISSION-ID'],
|
||||||
|
transmission_sig: headers['PAYPAL-TRANSMISSION-SIG'],
|
||||||
|
transmission_time: headers['PAYPAL-TRANSMISSION-TIME'],
|
||||||
|
webhook_id: webhookId,
|
||||||
|
webhook_event: webhookEventBody
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(verificationUrl, payload, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.verification_status === 'SUCCESS') {
|
||||||
|
return body;
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
message: `Failed token verification.`,
|
||||||
|
type: 'FailedPaypalTokenVerificationError'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyWebHookType(req) {
|
||||||
|
// check if webhook type for creation
|
||||||
|
const {
|
||||||
|
body: { event_type }
|
||||||
|
} = req;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event_type === 'BILLING.SUBSCRIPTION.ACTIVATED' ||
|
||||||
|
event_type === 'BILLING.SUBSCRIPTION.CANCELLED'
|
||||||
|
)
|
||||||
|
return req;
|
||||||
|
else
|
||||||
|
throw {
|
||||||
|
message: 'Webhook type is not supported',
|
||||||
|
type: 'UnsupportedWebhookType'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAsyncUserDonation = (user, donation) => {
|
||||||
|
log(`Creating donation:${donation.subscriptionId}`);
|
||||||
|
user
|
||||||
|
.createDonation(donation)
|
||||||
|
.toPromise()
|
||||||
|
.catch(err => {
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDonationObj(body) {
|
||||||
|
const {
|
||||||
|
resource: {
|
||||||
|
id,
|
||||||
|
start_time,
|
||||||
|
subscriber: { email_address } = {
|
||||||
|
email_address: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
let donation = {
|
||||||
|
email: email_address,
|
||||||
|
amount: 500,
|
||||||
|
duration: 'month',
|
||||||
|
provider: 'paypal',
|
||||||
|
subscriptionId: id,
|
||||||
|
customerId: email_address,
|
||||||
|
startDate: new Date(start_time).toISOString()
|
||||||
|
};
|
||||||
|
return donation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDonation(body, app) {
|
||||||
|
const { User } = app.models;
|
||||||
|
const {
|
||||||
|
resource: {
|
||||||
|
subscriber: { email_address } = {
|
||||||
|
email_address: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
let donation = createDonationObj(body);
|
||||||
|
|
||||||
|
let email = email_address;
|
||||||
|
return User.findOne({ where: { email } }, (err, user) => {
|
||||||
|
if (err) throw new Error(err);
|
||||||
|
if (!user) {
|
||||||
|
log(`Creating new user:${email}`);
|
||||||
|
return User.create({ email })
|
||||||
|
.then(user => {
|
||||||
|
createAsyncUserDonation(user, donation);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return createAsyncUserDonation(user, donation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancelDonation(body, app) {
|
||||||
|
const {
|
||||||
|
resource: { id, status_update_time = new Date(Date.now()).toISOString() }
|
||||||
|
} = body;
|
||||||
|
const { User, Donation } = app.models;
|
||||||
|
Donation.findOne({ where: { subscriptionId: id } }, (err, donation) => {
|
||||||
|
if (err || !donation) throw Error(err);
|
||||||
|
const userId = donation.userId;
|
||||||
|
log(`Updating donation record: ${donation.subscriptionId}`);
|
||||||
|
donation.updateAttributes({
|
||||||
|
endDate: new Date(status_update_time).toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
User.findOne({ where: { id: userId } }, (err, user) => {
|
||||||
|
if (err || !user || !user.donationEmails) throw Error(err);
|
||||||
|
log('Updating user record for donation cancellation');
|
||||||
|
user.updateAttributes({
|
||||||
|
isDonating: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(body, app) {
|
||||||
|
const { event_type } = body;
|
||||||
|
if (event_type === 'BILLING.SUBSCRIPTION.ACTIVATED') {
|
||||||
|
createDonation(body, app);
|
||||||
|
} else if (event_type === 'BILLING.SUBSCRIPTION.CANCELLED') {
|
||||||
|
cancelDonation(body, app);
|
||||||
|
} else
|
||||||
|
throw {
|
||||||
|
message: 'Webhook type is not supported',
|
||||||
|
type: 'UnsupportedWebhookType'
|
||||||
|
};
|
||||||
|
}
|
42
client/package-lock.json
generated
42
client/package-lock.json
generated
@ -18944,6 +18944,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-paypal-button-v2": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-paypal-button-v2/-/react-paypal-button-v2-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-ia1zzdRgziSfnJ/UueM6i0omEocbv4cge3mBRmpA6WSyxsX6c501HWHVwvXMEbTDCPK5+0ZKewhBzf/wq618Cg==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"rimraf": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"requires": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prop-types": {
|
||||||
|
"version": "15.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||||
|
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"react-is": {
|
||||||
|
"version": "16.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
||||||
|
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
|
||||||
|
},
|
||||||
|
"rimraf": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
|
"requires": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-prop-types": {
|
"react-prop-types": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-prop-types/-/react-prop-types-0.4.0.tgz",
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"react-identicons": "^1.1.7",
|
"react-identicons": "^1.1.7",
|
||||||
"react-instantsearch-dom": "^6.0.0-beta.0",
|
"react-instantsearch-dom": "^6.0.0-beta.0",
|
||||||
"react-monaco-editor": "^0.31.0",
|
"react-monaco-editor": "^0.31.0",
|
||||||
|
"react-paypal-button-v2": "^2.6.1",
|
||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-reflex": "^3.0.18",
|
"react-reflex": "^3.0.18",
|
||||||
"react-responsive": "^6.1.1",
|
"react-responsive": "^6.1.1",
|
||||||
|
@ -137,12 +137,12 @@ class ShowCertification extends Component {
|
|||||||
this.setState({ isDonationDisplayed: false, isDonationClosed: true });
|
this.setState({ isDonationDisplayed: false, isDonationClosed: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleProcessing(duration, amount) {
|
handleProcessing(duration, amount, action = 'stripe form submission') {
|
||||||
this.props.executeGA({
|
this.props.executeGA({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
data: {
|
data: {
|
||||||
category: 'donation',
|
category: 'donation',
|
||||||
action: 'certificate stripe form submission',
|
action: `certificate ${action}`,
|
||||||
label: duration,
|
label: duration,
|
||||||
value: amount
|
value: amount
|
||||||
}
|
}
|
||||||
|
@ -212,6 +212,10 @@ li.disabled > a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.donation-modal {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.donation-modal .btn-link:focus {
|
.donation-modal .btn-link:focus {
|
||||||
outline-width: 1px;
|
outline-width: 1px;
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
|
@ -59,12 +59,16 @@ function DonateModal({
|
|||||||
executeGA
|
executeGA
|
||||||
}) {
|
}) {
|
||||||
const [closeLabel, setCloseLabel] = React.useState(false);
|
const [closeLabel, setCloseLabel] = React.useState(false);
|
||||||
const handleProcessing = (duration, amount) => {
|
const handleProcessing = (
|
||||||
|
duration,
|
||||||
|
amount,
|
||||||
|
action = 'stripe form submission'
|
||||||
|
) => {
|
||||||
executeGA({
|
executeGA({
|
||||||
type: 'event',
|
type: 'event',
|
||||||
data: {
|
data: {
|
||||||
category: 'donation',
|
category: 'donation',
|
||||||
action: 'Modal strip form submission',
|
action: `Modal ${action}`,
|
||||||
label: duration,
|
label: duration,
|
||||||
value: amount
|
value: amount
|
||||||
}
|
}
|
||||||
@ -88,8 +92,8 @@ function DonateModal({
|
|||||||
|
|
||||||
const donationText = (
|
const donationText = (
|
||||||
<b>
|
<b>
|
||||||
Become a supporter and help us create even more learning resources for
|
Become a $5 / month supporter and help us create even more learning
|
||||||
you.
|
resources for you and your family.
|
||||||
</b>
|
</b>
|
||||||
);
|
);
|
||||||
const blockDonationText = (
|
const blockDonationText = (
|
||||||
|
@ -13,8 +13,12 @@ import {
|
|||||||
import { stripePublicKey } from '../../../../config/env.json';
|
import { stripePublicKey } from '../../../../config/env.json';
|
||||||
import { stripeScriptLoader } from '../../utils/scriptLoaders';
|
import { stripeScriptLoader } from '../../utils/scriptLoaders';
|
||||||
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
|
import DonateFormChildViewForHOC from './DonateFormChildViewForHOC';
|
||||||
|
import DonateCompletion from './DonateCompletion';
|
||||||
|
import PaypalButton from './PaypalButton';
|
||||||
import { userSelector } from '../../redux';
|
import { userSelector } from '../../redux';
|
||||||
|
|
||||||
|
import { Spacer } from '../../components/helpers';
|
||||||
|
|
||||||
import './Donation.css';
|
import './Donation.css';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -33,6 +37,14 @@ const mapStateToProps = createSelector(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
donationState: {
|
||||||
|
processing: false,
|
||||||
|
success: false,
|
||||||
|
error: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class MinimalDonateForm extends Component {
|
class MinimalDonateForm extends Component {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
@ -42,10 +54,13 @@ class MinimalDonateForm extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
...modalDefaultStateConfig,
|
...modalDefaultStateConfig,
|
||||||
|
...initialState,
|
||||||
isDonating: this.props.isDonating,
|
isDonating: this.props.isDonating,
|
||||||
stripe: null
|
stripe: null
|
||||||
};
|
};
|
||||||
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
this.handleStripeLoad = this.handleStripeLoad.bind(this);
|
||||||
|
this.onDonationStateChange = this.onDonationStateChange.bind(this);
|
||||||
|
this.resetDonation = this.resetDonation.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -77,12 +92,54 @@ class MinimalDonateForm extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetDonation() {
|
||||||
|
return this.setState({ ...initialState });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDonationStateChange(success, processing, error) {
|
||||||
|
this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
donationState: {
|
||||||
|
...state.donationState,
|
||||||
|
processing: processing,
|
||||||
|
success: success,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCompletion(props) {
|
||||||
|
return <DonateCompletion {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { donationAmount, donationDuration, stripe } = this.state;
|
const { donationAmount, donationDuration, stripe } = this.state;
|
||||||
const { handleProcessing, defaultTheme } = this.props;
|
const { handleProcessing, defaultTheme } = this.props;
|
||||||
|
const {
|
||||||
|
donationState: { processing, success, error }
|
||||||
|
} = this.state;
|
||||||
|
if (processing || success || error) {
|
||||||
|
return this.renderCompletion({
|
||||||
|
processing,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
reset: this.resetDonation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
|
<Col lg={8} lgOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
<PaypalButton
|
||||||
|
handleProcessing={handleProcessing}
|
||||||
|
onDonationStateChange={this.onDonationStateChange}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col sm={10} smOffset={1} xs={12}>
|
||||||
|
<Spacer />
|
||||||
|
<b>Or donate with credit card number.</b>
|
||||||
|
<Spacer />
|
||||||
|
</Col>
|
||||||
<Col sm={10} smOffset={1} xs={12}>
|
<Col sm={10} smOffset={1} xs={12}>
|
||||||
<StripeProvider stripe={stripe}>
|
<StripeProvider stripe={stripe}>
|
||||||
<Elements>
|
<Elements>
|
||||||
@ -91,7 +148,7 @@ class MinimalDonateForm extends Component {
|
|||||||
donationAmount={donationAmount}
|
donationAmount={donationAmount}
|
||||||
donationDuration={donationDuration}
|
donationDuration={donationDuration}
|
||||||
getDonationButtonLabel={() =>
|
getDonationButtonLabel={() =>
|
||||||
`Confirm your donation of $5 per month`
|
`Confirm your donation of $5 / month`
|
||||||
}
|
}
|
||||||
handleProcessing={handleProcessing}
|
handleProcessing={handleProcessing}
|
||||||
/>
|
/>
|
||||||
|
112
client/src/components/Donation/PaypalButton.js
Normal file
112
client/src/components/Donation/PaypalButton.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
/* global ENVIRONMENT */
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { PayPalButton } from 'react-paypal-button-v2';
|
||||||
|
import { paypalClientId } from '../../../config/env.json';
|
||||||
|
import { verifySubscriptionPaypal } from '../../utils/ajax';
|
||||||
|
import { paypalConfig } from '../../../../config/donation-settings';
|
||||||
|
import { signInLoadingSelector, userSelector, executeGA } from '../../redux';
|
||||||
|
|
||||||
|
const paypalDurationPlans =
|
||||||
|
ENVIRONMENT === 'production'
|
||||||
|
? paypalConfig.production.durationPlans
|
||||||
|
: paypalConfig.development.durationPlans;
|
||||||
|
|
||||||
|
export class PaypalButton extends Component {
|
||||||
|
constructor(...props) {
|
||||||
|
super(...props);
|
||||||
|
this.state = {
|
||||||
|
planId: paypalDurationPlans.month['500'].planId
|
||||||
|
};
|
||||||
|
this.handleApproval = this.handleApproval.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleApproval = data => {
|
||||||
|
this.props.handleProcessing('month', 500, 'Paypal payment submission');
|
||||||
|
this.props.onDonationStateChange(false, true, '');
|
||||||
|
verifySubscriptionPaypal(data)
|
||||||
|
.then(response => {
|
||||||
|
const data = response && response.data;
|
||||||
|
this.props.onDonationStateChange(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
data.error ? data.error : ''
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const data =
|
||||||
|
error.response && error.response.data
|
||||||
|
? error.response.data
|
||||||
|
: {
|
||||||
|
error:
|
||||||
|
'Something is not right. Please contact team@freecodecamp.org'
|
||||||
|
};
|
||||||
|
this.props.onDonationStateChange(false, false, data.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PayPalButton
|
||||||
|
createSubscription={(data, actions) => {
|
||||||
|
executeGA({
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
category: 'Donation',
|
||||||
|
action: `Modal Paypal clicked`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return actions.subscription.create({
|
||||||
|
plan_id: this.state.planId
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onApprove={data => {
|
||||||
|
this.handleApproval(data);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
this.props.onDonationStateChange(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
'Payment has been canceled.'
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onError={() =>
|
||||||
|
this.props.onDonationStateChange(false, false, 'Please try again.')
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
vault: true,
|
||||||
|
disableFunding: 'card',
|
||||||
|
clientId: paypalClientId
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
tagline: false,
|
||||||
|
height: 43
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
handleProcessing: PropTypes.func,
|
||||||
|
isDonating: PropTypes.bool,
|
||||||
|
onDonationStateChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
signInLoadingSelector,
|
||||||
|
({ isDonating }, showLoading) => ({
|
||||||
|
isDonating,
|
||||||
|
showLoading
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
PaypalButton.displayName = 'PaypalButton';
|
||||||
|
PaypalButton.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PaypalButton);
|
@ -42,6 +42,10 @@
|
|||||||
padding: 20px 0px;
|
padding: 20px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.certificate-outer-wrapper .donation-section hr {
|
||||||
|
border: 1px solid var(--gray-10);
|
||||||
|
}
|
||||||
|
|
||||||
.certificate-outer-wrapper .donation-completion .btn {
|
.certificate-outer-wrapper .donation-completion .btn {
|
||||||
background-color: var(--gray-15);
|
background-color: var(--gray-15);
|
||||||
border-color: var(--gray-85);
|
border-color: var(--gray-85);
|
||||||
@ -61,6 +65,11 @@
|
|||||||
color: var(--quaternary-color);
|
color: var(--quaternary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.certificate-outer-wrapper .donation-section,
|
||||||
|
.certificate-outer-wrapper .donation-section p {
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
.certification-namespace header {
|
.certification-namespace header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
|
@ -12,6 +12,7 @@ import DonateForm from '../components/Donation/DonateForm';
|
|||||||
import DonateText from '../components/Donation/DonateText';
|
import DonateText from '../components/Donation/DonateText';
|
||||||
import { signInLoadingSelector, userSelector, executeGA } from '../redux';
|
import { signInLoadingSelector, userSelector, executeGA } from '../redux';
|
||||||
import { stripeScriptLoader } from '../utils/scriptLoaders';
|
import { stripeScriptLoader } from '../utils/scriptLoaders';
|
||||||
|
import { PaypalButton } from '../components/Donation/PaypalButton';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
executeGA: PropTypes.func,
|
executeGA: PropTypes.func,
|
||||||
@ -118,6 +119,9 @@ export class DonatePage extends Component {
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<PaypalButton />
|
||||||
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
{isDonating ? (
|
{isDonating ? (
|
||||||
<Col md={6} mdOffset={3}>
|
<Col md={6} mdOffset={3}>
|
||||||
|
@ -54,6 +54,10 @@ export function postChargeStripe(body) {
|
|||||||
return post('/donate/charge-stripe', body);
|
return post('/donate/charge-stripe', body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function verifySubscriptionPaypal(body) {
|
||||||
|
return post('/donate/add-donation', body);
|
||||||
|
}
|
||||||
|
|
||||||
export function postCreateHmacHash(body) {
|
export function postCreateHmacHash(body) {
|
||||||
return post(`/donate/create-hmac-hash`, body);
|
return post(`/donate/create-hmac-hash`, body);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
/* global describe it expect */
|
/* global expect */
|
||||||
import { isObject } from 'lodash';
|
import { isObject } from 'lodash';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import {
|
import {
|
||||||
|
@ -39,6 +39,30 @@ const donationSubscriptionConfig = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Shared paypal configuration
|
||||||
|
const paypalConfig = {
|
||||||
|
production: {
|
||||||
|
webhookId: '8AM40465WC915574A',
|
||||||
|
durationPlans: {
|
||||||
|
month: {
|
||||||
|
'500': {
|
||||||
|
planId: 'P-1L11422374370240ULZKX3PA'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
webhookId: '2UL63757DN298592C',
|
||||||
|
durationPlans: {
|
||||||
|
month: {
|
||||||
|
'500': {
|
||||||
|
planId: 'P-146249205C631091BLZKRHGA'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
durationsConfig,
|
durationsConfig,
|
||||||
amountsConfig,
|
amountsConfig,
|
||||||
@ -47,5 +71,6 @@ module.exports = {
|
|||||||
durationKeysConfig,
|
durationKeysConfig,
|
||||||
donationOneTimeConfig,
|
donationOneTimeConfig,
|
||||||
donationSubscriptionConfig,
|
donationSubscriptionConfig,
|
||||||
modalDefaultStateConfig
|
modalDefaultStateConfig,
|
||||||
|
paypalConfig
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,8 @@ const {
|
|||||||
STRIPE_PUBLIC_KEY: stripePublicKey,
|
STRIPE_PUBLIC_KEY: stripePublicKey,
|
||||||
SERVICEBOT_ID: servicebotId,
|
SERVICEBOT_ID: servicebotId,
|
||||||
ALGOLIA_APP_ID: algoliaAppId,
|
ALGOLIA_APP_ID: algoliaAppId,
|
||||||
ALGOLIA_API_KEY: algoliaAPIKey
|
ALGOLIA_API_KEY: algoliaAPIKey,
|
||||||
|
PAYPAL_CLIENT_ID: paypalClientId
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
const locations = {
|
const locations = {
|
||||||
@ -49,5 +50,9 @@ module.exports = Object.assign(locations, {
|
|||||||
algoliaAPIKey:
|
algoliaAPIKey:
|
||||||
!algoliaAPIKey || algoliaAPIKey === 'Algolia api key from dashboard'
|
!algoliaAPIKey || algoliaAPIKey === 'Algolia api key from dashboard'
|
||||||
? null
|
? null
|
||||||
: algoliaAPIKey
|
: algoliaAPIKey,
|
||||||
|
paypalClientId:
|
||||||
|
!paypalClientId || paypalClientId === 'id_from_paypal_dashboard'
|
||||||
|
? null
|
||||||
|
: paypalClientId
|
||||||
});
|
});
|
||||||
|
@ -33,7 +33,11 @@ const {
|
|||||||
STRIPE_PUBLIC_KEY,
|
STRIPE_PUBLIC_KEY,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
SERVICEBOT_ID,
|
SERVICEBOT_ID,
|
||||||
SERVICEBOT_HMAC_SECRET_KEY
|
|
||||||
|
SERVICEBOT_HMAC_SECRET_KEY,
|
||||||
|
|
||||||
|
PAYPAL_CLIENT_ID,
|
||||||
|
PAYPAL_SECRET
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -98,6 +102,11 @@ module.exports = {
|
|||||||
secret: STRIPE_SECRET_KEY
|
secret: STRIPE_SECRET_KEY
|
||||||
},
|
},
|
||||||
|
|
||||||
|
paypal: {
|
||||||
|
client: PAYPAL_CLIENT_ID,
|
||||||
|
secret: PAYPAL_SECRET
|
||||||
|
},
|
||||||
|
|
||||||
servicebot: {
|
servicebot: {
|
||||||
servicebotId: SERVICEBOT_ID,
|
servicebotId: SERVICEBOT_ID,
|
||||||
hmacKey: SERVICEBOT_HMAC_SECRET_KEY
|
hmacKey: SERVICEBOT_HMAC_SECRET_KEY
|
||||||
|
@ -23,6 +23,8 @@ SERVICEBOT_ID=servicebot_id_from_servicebot_dashboard
|
|||||||
SERVICEBOT_HMAC_SECRET_KEY=secret_key_from_servicebot_dashboard
|
SERVICEBOT_HMAC_SECRET_KEY=secret_key_from_servicebot_dashboard
|
||||||
|
|
||||||
PAYPAL_SUPPORTERS=1703
|
PAYPAL_SUPPORTERS=1703
|
||||||
|
PAYPAL_CLIENT_ID=id_from_paypal_dashboard
|
||||||
|
PAYPAL_SECRET=secret_from_paypal_dashboard
|
||||||
|
|
||||||
PEER=stuff
|
PEER=stuff
|
||||||
DEBUG=true
|
DEBUG=true
|
||||||
|
Reference in New Issue
Block a user