diff --git a/packages/nodes-base/nodes/PayPal/GenericFunctions.ts b/packages/nodes-base/nodes/PayPal/GenericFunctions.ts index 28dc39e01a..b1b5a20914 100644 --- a/packages/nodes-base/nodes/PayPal/GenericFunctions.ts +++ b/packages/nodes-base/nodes/PayPal/GenericFunctions.ts @@ -5,6 +5,7 @@ import { IHookFunctions, ILoadOptionsFunctions, IExecuteSingleFunctions, + IWebhookFunctions, BINARY_ENCODING } from 'n8n-core'; @@ -12,7 +13,7 @@ import { IDataObject, } from 'n8n-workflow'; -export async function payPalApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any +export async function payPalApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('payPalApi'); const env = getEnviroment(credentials!.env as string); const tokenInfo = await getAccessToken.call(this); @@ -29,12 +30,16 @@ export async function payPalApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message; - if (errorMessage !== undefined) { - throw errorMessage; + if (error.response.body) { + let errorMessage = error.response.body.message; + if (error.response.body.details) { + errorMessage += ` - Details: ${JSON.stringify(error.response.body.details)}`; + } + throw new Error(errorMessage); } - throw error.response.body; + + throw error; } } @@ -42,11 +47,11 @@ function getEnviroment(env: string): string { // @ts-ignore return { 'sanbox': 'https://api.sandbox.paypal.com', - 'live': 'https://api.paypal.com' + 'live': 'https://api.paypal.com', }[env]; } -async function getAccessToken(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions): Promise { // tslint:disable-line:no-any +async function getAccessToken(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('payPalApi'); if (credentials === undefined) { throw new Error('No credentials got returned!'); @@ -70,9 +75,9 @@ async function getAccessToken(this: IHookFunctions | IExecuteFunctions | IExecut const errorMessage = error.response.body.message || error.response.body.Message; if (errorMessage !== undefined) { - throw errorMessage; + throw new Error(errorMessage); } - throw error.response.body; + throw new Error(error.response.body); } } @@ -117,3 +122,9 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } + +export function upperFist(s: string): string { + return s.split('.').map(e => { + return e.toLowerCase().charAt(0).toUpperCase() + e.toLowerCase().slice(1); + }).join(' '); +} diff --git a/packages/nodes-base/nodes/PayPal/PayPal.node.ts b/packages/nodes-base/nodes/PayPal/PayPal.node.ts index 9f664a46ee..2f1f00147d 100644 --- a/packages/nodes-base/nodes/PayPal/PayPal.node.ts +++ b/packages/nodes-base/nodes/PayPal/PayPal.node.ts @@ -21,9 +21,9 @@ import { RecipientWallet, } from './PaymentInteface'; import { - validateJSON, payPalApiRequest, - payPalApiRequestAllItems + payPalApiRequestAllItems, + validateJSON, } from './GenericFunctions'; export class PayPal implements INodeType { diff --git a/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts b/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts new file mode 100644 index 0000000000..e12d7dd004 --- /dev/null +++ b/packages/nodes-base/nodes/PayPal/PayPalTrigger.node.ts @@ -0,0 +1,201 @@ +import { + IHookFunctions, + IWebhookFunctions, + } from 'n8n-core'; + + import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, + } from 'n8n-workflow'; + import { + payPalApiRequest, + upperFist + } from './GenericFunctions'; + + export class PayPalTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'PayPal Trigger', + name: 'PayPal', + icon: 'file:paypal.png', + group: ['trigger'], + version: 1, + description: 'Handle PayPal events via webhooks', + defaults: { + name: 'PayPal Trigger', + color: '#32325d', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'payPalApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + reponseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + description: 'The event to listen to.', + typeOptions: { + loadOptionsMethod: 'getEvents' + }, + options: [], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the events types to display them to user so that he can + // select them easily + async getEvents(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = [ + { + name: '*', + value: '*', + description: 'Any time any event is triggered (Wildcard Event).', + } + ]; + let events; + try { + const endpoint = '/notifications/webhooks-event-types'; + events = await payPalApiRequest.call(this, endpoint, 'GET'); + } catch (err) { + throw new Error(`PayPal Error: ${err}`); + } + for (const event of events.event_types) { + const eventName = upperFist(event.name); + const eventId = event.name; + const eventDescription = event.description; + + returnData.push({ + name: eventName, + value: eventId, + description: eventDescription, + }); + } + return returnData; + }, + }, + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; + try { + await payPalApiRequest.call(this, endpoint, 'GET'); + } catch (err) { + if (err.response && err.response.name === 'INVALID_RESOURCE_ID') { + // Webhook does not exist + delete webhookData.webhookId; + return false; + } + throw new Error(`PayPal Error: ${err}`); + } + return true; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const events = this.getNodeParameter('events', []) as string[]; + const body = { + url: webhookUrl, + event_types: events.map(event => { + return { name: event }; + }), + }; + const endpoint = '/notifications/webhooks'; + try { + webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + } catch (e) { + throw e; + } + + if (webhook.id === undefined) { + return false; + } + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.id as string; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/notifications/webhooks/${webhookData.webhookId}`; + try { + await payPalApiRequest.call(this, endpoint, 'DELETE', {}); + } catch (e) { + return false; + } + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + let webhook; + const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const bodyData = this.getBodyData() as IDataObject; + const req = this.getRequestObject(); + const headerData = this.getHeaderData() as IDataObject; + const endpoint = '/notifications/verify-webhook-signature'; + + if (headerData['PAYPAL-AUTH-ALGO'] !== undefined + && headerData['PAYPAL-CERT-URL'] !== undefined + && headerData['PAYPAL-TRANSMISSION-ID'] !== undefined + && headerData['PAYPAL-TRANSMISSION-SIG'] !== undefined + && headerData['PAYPAL-TRANSMISSION-TIME'] !== undefined) { + const body = { + auth_algo: headerData['PAYPAL-AUTH-ALGO'], + cert_url: headerData['PAYPAL-CERT-URL'], + transmission_id: headerData['PAYPAL-TRANSMISSION-ID'], + transmission_sig: headerData['PAYPAL-TRANSMISSION-SIG'], + transmission_time: headerData['PAYPAL-TRANSMISSION-TIME'], + webhook_id: webhookData.webhookId, + webhook_event: bodyData, + }; + try { + webhook = await payPalApiRequest.call(this, endpoint, 'POST', body); + } catch (e) { + throw e; + } + if (webhook.verification_status !== 'SUCCESS') { + return {}; + } + } else { + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } + } diff --git a/packages/nodes-base/nodes/PayPal/paypal.png b/packages/nodes-base/nodes/PayPal/paypal.png index 01891251ef..64e1218880 100644 Binary files a/packages/nodes-base/nodes/PayPal/paypal.png and b/packages/nodes-base/nodes/PayPal/paypal.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 92a4f0e63d..6e77260024 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -117,6 +117,7 @@ "dist/nodes/Pipedrive/PipedriveTrigger.node.js", "dist/nodes/Postgres/Postgres.node.js", "dist/nodes/PayPal/PayPal.node.js", + "dist/nodes/PayPal/PayPalTrigger.node.js", "dist/nodes/Rocketchat/Rocketchat.node.js", "dist/nodes/ReadBinaryFile.node.js", "dist/nodes/ReadBinaryFiles.node.js",