diff --git a/.devcontainer/advanced-integration-v1/devcontainer.json b/.devcontainer/advanced-integration-v1/devcontainer.json new file mode 100644 index 00000000..9ce61d30 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v1", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/advanced-integration/v1", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../../.devcontainer/advanced-integration-v1/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Advanced Checkout Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration-v1/welcome-message.sh b/.devcontainer/advanced-integration-v1/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v1/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-html-dotnet/devcontainer.json b/.devcontainer/advanced-integration-v2-html-dotnet/devcontainer.json new file mode 100644 index 00000000..2ef1ead0 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-dotnet/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/html/dotnet", + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-html-dotnet/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "dotnet", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "ms-dotnettools.csharp" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/advanced-integration-v2-html-dotnet/welcome-message.sh b/.devcontainer/advanced-integration-v2-html-dotnet/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-dotnet/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-html-java/devcontainer.json b/.devcontainer/advanced-integration-v2-html-java/devcontainer.json new file mode 100644 index 00000000..3dcae617 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-java/devcontainer.json @@ -0,0 +1,57 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/html/java", + "image": "mcr.microsoft.com/devcontainers/java:21", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-html-java/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "java", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour", "PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "22", + "jdkDistro": "tem", + "installMaven": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration-v2-html-java/welcome-message.sh b/.devcontainer/advanced-integration-v2-html-java/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-java/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-html-node/devcontainer.json b/.devcontainer/advanced-integration-v2-html-node/devcontainer.json new file mode 100644 index 00000000..f5f00400 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-node/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/html/node", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-html-node/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "node", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "dbaeumer.vscode-eslint" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/advanced-integration-v2-html-node/welcome-message.sh b/.devcontainer/advanced-integration-v2-html-node/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-node/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-html-php/devcontainer.json b/.devcontainer/advanced-integration-v2-html-php/devcontainer.json new file mode 100644 index 00000000..6afcc17b --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-php/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/html/php", + "image": "mcr.microsoft.com/devcontainers/php:8", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-html-php/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "php", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "xdebug.php-debug" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration-v2-html-php/welcome-message.sh b/.devcontainer/advanced-integration-v2-html-php/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-html-php/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-react-dotnet/devcontainer.json b/.devcontainer/advanced-integration-v2-react-dotnet/devcontainer.json new file mode 100644 index 00000000..a4911ab0 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-dotnet/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": " advanced-integration-v2/react/dotnet", + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-react-dotnet/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "dotnet", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "ms-dotnettools.csharp" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/advanced-integration-v2-react-dotnet/welcome-message.sh b/.devcontainer/advanced-integration-v2-react-dotnet/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-dotnet/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-react-java/devcontainer.json b/.devcontainer/advanced-integration-v2-react-java/devcontainer.json new file mode 100644 index 00000000..e55feb54 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-java/devcontainer.json @@ -0,0 +1,57 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/react/java", + "image": "mcr.microsoft.com/devcontainers/java:21", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-react-java/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "java", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour", "PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "22", + "jdkDistro": "tem", + "installMaven": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration-v2-react-java/welcome-message.sh b/.devcontainer/advanced-integration-v2-react-java/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-java/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-react-node/devcontainer.json b/.devcontainer/advanced-integration-v2-react-node/devcontainer.json new file mode 100644 index 00000000..2efab718 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-node/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/react/node", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-react-node/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "node", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "dbaeumer.vscode-eslint" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/advanced-integration-v2-react-node/welcome-message.sh b/.devcontainer/advanced-integration-v2-react-node/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-node/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/advanced-integration-v2-react-php/devcontainer.json b/.devcontainer/advanced-integration-v2-react-php/devcontainer.json new file mode 100644 index 00000000..4980fd30 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-php/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "advanced-integration-v2/react/php", + "image": "mcr.microsoft.com/devcontainers/php:8", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/advanced-integration-v2-react-php/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Advanced Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "php", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "advanced-integration", + "VISIBLE_FOLDER_VERSION": "v2" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "xdebug.php-debug" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/advanced-integration-v2-react-php/welcome-message.sh b/.devcontainer/advanced-integration-v2-react-php/welcome-message.sh new file mode 100644 index 00000000..ae9a72f9 --- /dev/null +++ b/.devcontainer/advanced-integration-v2-react-php/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Advanced Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/post-commands.sh b/.devcontainer/post-commands.sh new file mode 100755 index 00000000..505bdf50 --- /dev/null +++ b/.devcontainer/post-commands.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +WORKSPACE_DIR="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" + +VISIBLE_FOLDER_SERVER="${VISIBLE_FOLDER_SERVER}" +VISIBLE_FOLDER_CLIENT="${VISIBLE_FOLDER_CLIENT}" +VISIBLE_FOLDER_PROJECT="${VISIBLE_FOLDER_PROJECT}" +VISIBLE_FOLDER_VERSION="${VISIBLE_FOLDER_VERSION}" + +# Set up SERVER_DIR & CLIENT_DIR +if [ -z "$VISIBLE_FOLDER_VERSION" ]; then + SERVER_DIR="${WORKSPACE_DIR}/${VISIBLE_FOLDER_PROJECT}/server" + CLIENT_DIR="${WORKSPACE_DIR}/${VISIBLE_FOLDER_PROJECT}/client/${VISIBLE_FOLDER_CLIENT}" +else + SERVER_DIR="${WORKSPACE_DIR}/${VISIBLE_FOLDER_PROJECT}/${VISIBLE_FOLDER_VERSION}/server" + CLIENT_DIR="${WORKSPACE_DIR}/${VISIBLE_FOLDER_PROJECT}/${VISIBLE_FOLDER_VERSION}/client/${VISIBLE_FOLDER_CLIENT}" +fi + +# Backend setup functions +setup_backend() { + case "$VISIBLE_FOLDER_SERVER" in + node) + cd "$SERVER_DIR/node" && npm install + ;; + java) + cd "$SERVER_DIR" && cd java && mvn clean install + ;; + dotnet) + cd "$SERVER_DIR/dotnet" && dotnet restore + ;; + php) + cd "$SERVER_DIR" && cd php && composer install + ;; + *) + echo "Unknown server: $VISIBLE_FOLDER_SERVER" + exit 1 + ;; + esac +} + +# Frontend setup functions +setup_frontend() { + cd "$CLIENT_DIR" && npm install +} + +# Backend start functions +start_backend() { + case "$VISIBLE_FOLDER_SERVER" in + node) + cd "$SERVER_DIR/node" && npm start + ;; + java) + cd "$SERVER_DIR/java" && mvn spring-boot:run + ;; + dotnet) + cd "$SERVER_DIR/dotnet" && dotnet run + ;; + php) + cd "$SERVER_DIR/php" && composer start + ;; + *) + echo "Unknown server: $VISIBLE_FOLDER_SERVER" + exit 1 + ;; + esac +} + +# Frontend start functions +start_frontend() { + cd "$CLIENT_DIR" && npm run start --no-analytics +} + +# Post-create commands +post_create() { + echo "Running post-create commands..." + setup_backend + setup_frontend +} + +# Post-attach commands +post_attach() { + echo "Running post-attach commands..." + start_backend & + start_frontend +} + +# Main execution +case "$1" in + post-create) + post_create + ;; + post-attach) + post_attach + ;; + *) + echo "Usage: $0 {post-create|post-attach}" + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/.devcontainer/save-payment-method/devcontainer.json b/.devcontainer/save-payment-method/devcontainer.json new file mode 100644 index 00000000..88b05faa --- /dev/null +++ b/.devcontainer/save-payment-method/devcontainer.json @@ -0,0 +1,40 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "save-payment-metthod", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}/save-payment-method", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash ../.devcontainer/save-payment-method/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": { + "Start server": "npm start" + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8888], + "portsAttributes": { + "8888": { + "label": "Preview of Save Payment Method Flow", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour","PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + } +} \ No newline at end of file diff --git a/.devcontainer/save-payment-method/welcome-message.sh b/.devcontainer/save-payment-method/welcome-message.sh new file mode 100644 index 00000000..7ed0d59c --- /dev/null +++ b/.devcontainer/save-payment-method/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Save Payment Method Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Save Payment Method Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" \ No newline at end of file diff --git a/.devcontainer/standard-integration-html-dotnet/devcontainer.json b/.devcontainer/standard-integration-html-dotnet/devcontainer.json new file mode 100644 index 00000000..810ffa39 --- /dev/null +++ b/.devcontainer/standard-integration-html-dotnet/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/html/dotnet", + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-html-dotnet/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "dotnet", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "ms-dotnettools.csharp" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/standard-integration-html-dotnet/welcome-message.sh b/.devcontainer/standard-integration-html-dotnet/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-html-dotnet/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-html-java/devcontainer.json b/.devcontainer/standard-integration-html-java/devcontainer.json new file mode 100644 index 00000000..88f7280a --- /dev/null +++ b/.devcontainer/standard-integration-html-java/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/html/java", + "image": "mcr.microsoft.com/devcontainers/java:21", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-html-java/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "Html", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "java", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour", "PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "22", + "jdkDistro": "tem", + "installMaven": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/standard-integration-html-java/welcome-message.sh b/.devcontainer/standard-integration-html-java/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-html-java/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-html-node/devcontainer.json b/.devcontainer/standard-integration-html-node/devcontainer.json new file mode 100644 index 00000000..beaa4863 --- /dev/null +++ b/.devcontainer/standard-integration-html-node/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/html/node", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-html-node/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "node", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "dbaeumer.vscode-eslint" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/standard-integration-html-node/welcome-message.sh b/.devcontainer/standard-integration-html-node/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-html-node/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-html-php/devcontainer.json b/.devcontainer/standard-integration-html-php/devcontainer.json new file mode 100644 index 00000000..fc60c8f7 --- /dev/null +++ b/.devcontainer/standard-integration-html-php/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/html/php", + "image": "mcr.microsoft.com/devcontainers/php:8", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-html-php/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "HTML", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "php", + "VISIBLE_FOLDER_CLIENT": "html", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "xdebug.php-debug" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/standard-integration-html-php/welcome-message.sh b/.devcontainer/standard-integration-html-php/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-html-php/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-react-dotnet/devcontainer.json b/.devcontainer/standard-integration-react-dotnet/devcontainer.json new file mode 100644 index 00000000..d2a2e587 --- /dev/null +++ b/.devcontainer/standard-integration-react-dotnet/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/react/dotnet", + "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-react-dotnet/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "dotnet", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "ms-dotnettools.csharp" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/standard-integration-react-dotnet/welcome-message.sh b/.devcontainer/standard-integration-react-dotnet/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-react-dotnet/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-react-java/devcontainer.json b/.devcontainer/standard-integration-react-java/devcontainer.json new file mode 100644 index 00000000..d9f5d333 --- /dev/null +++ b/.devcontainer/standard-integration-react-java/devcontainer.json @@ -0,0 +1,56 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/react/java", + "image": "mcr.microsoft.com/devcontainers/java:21", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-react-java/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "java", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": ["vsls-contrib.codetour", "PayPal.vscode-paypal"], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/java:1": { + "version": "22", + "jdkDistro": "tem", + "installMaven": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/standard-integration-react-java/welcome-message.sh b/.devcontainer/standard-integration-react-java/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-react-java/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-react-node/devcontainer.json b/.devcontainer/standard-integration-react-node/devcontainer.json new file mode 100644 index 00000000..fa64ab68 --- /dev/null +++ b/.devcontainer/standard-integration-react-node/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/react/node", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-react-node/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "node", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "dbaeumer.vscode-eslint" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} diff --git a/.devcontainer/standard-integration-react-node/welcome-message.sh b/.devcontainer/standard-integration-react-node/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-react-node/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/standard-integration-react-php/devcontainer.json b/.devcontainer/standard-integration-react-php/devcontainer.json new file mode 100644 index 00000000..e5bfcf12 --- /dev/null +++ b/.devcontainer/standard-integration-react-php/devcontainer.json @@ -0,0 +1,55 @@ +// For more details, see https://aka.ms/devcontainer.json. +{ + "name": "standard-integration/react/php", + "image": "mcr.microsoft.com/devcontainers/php:8", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Use 'onCreateCommand' to run commands when creating the container. + "onCreateCommand": "bash .devcontainer/standard-integration-react-php/welcome-message.sh", + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-create", + // Use 'postAttachCommand' to run commands when attaching to the container. + "postAttachCommand": "chmod +x .devcontainer/post-commands.sh && .devcontainer/post-commands.sh post-attach", + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000, 8080], + "portsAttributes": { + "8080": { + "label": "Preview of Standard Checkout Flow" + }, + "3000": { + "label": "React", + "onAutoForward": "openBrowserOnce" + } + }, + "secrets": { + "PAYPAL_CLIENT_ID": { + "description": "Sandbox client ID of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + }, + "PAYPAL_CLIENT_SECRET": { + "description": "Sandbox secret of the application.", + "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" + } + }, + "containerEnv": { + "VISIBLE_FOLDER_SERVER": "php", + "VISIBLE_FOLDER_CLIENT": "react", + "VISIBLE_FOLDER_PROJECT": "standard-integration" + }, + "customizations": { + "vscode": { + "extensions": [ + "vsls-contrib.codetour", + "PayPal.vscode-paypal", + "xdebug.php-debug" + ], + "settings": { + "git.openRepositoryInParentFolders": "always" + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + } +} \ No newline at end of file diff --git a/.devcontainer/standard-integration-react-php/welcome-message.sh b/.devcontainer/standard-integration-react-php/welcome-message.sh new file mode 100644 index 00000000..78cce216 --- /dev/null +++ b/.devcontainer/standard-integration-react-php/welcome-message.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -e + +WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 Once you rename the \".env.example\" file to \".env\" and update \"PAYPAL_CLIENT_ID\" and \"PAYPAL_CLIENT_SECRET\", the checkout page will automatically open in the browser after the server is restarted." + +ALTERNATE_WELCOME_MESSAGE=" +👋 Welcome to the \"PayPal Standard Checkout Integration Example\" + +🛠️ Your environment is fully setup with all the required software. + +🚀 The checkout page will automatically open in the browser after the server is started." + +if [ -n "$PAYPAL_CLIENT_ID" ] && [ -n "$PAYPAL_CLIENT_SECRET" ]; then + WELCOME_MESSAGE="${ALTERNATE_WELCOME_MESSAGE}" +fi + +sudo bash -c "echo \"${WELCOME_MESSAGE}\" > /usr/local/etc/vscode-dev-containers/first-run-notice.txt" diff --git a/.devcontainer/update_settings.sh b/.devcontainer/update_settings.sh new file mode 100755 index 00000000..736b30ff --- /dev/null +++ b/.devcontainer/update_settings.sh @@ -0,0 +1,140 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +WORKSPACE_DIR="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" + +VISIBLE_FOLDER_SERVER="$VISIBLE_FOLDER_SERVER" +VISIBLE_FOLDER_CLIENT="$VISIBLE_FOLDER_CLIENT" +VISIBLE_FOLDER_PROJECT="$VISIBLE_FOLDER_PROJECT" +VISIBLE_FOLDER_VERSION="$VISIBLE_FOLDER_VERSION" + +DEVCONTAINER_WORKSPACE="$WORKSPACE_DIR/.devcontainer" +SETTINGS_FILE="$WORKSPACE_DIR/.vscode/settings.json" +PROJECT_WORKSPACE="$WORKSPACE_DIR/$VISIBLE_FOLDER_PROJECT" + + +if [ -z "$VISIBLE_FOLDER_CLIENT" ]; then + echo "Error: VISIBLE_FOLDER_CLIENT is not set, setting it to default" + VISIBLE_FOLDER_CLIENT="DEFAULT" +fi + +if [ -z "$VISIBLE_FOLDER_VERSION" ]; then + SERVER_WORKSPACE="$WORKSPACE_DIR/$VISIBLE_FOLDER_PROJECT/server" + CLIENT_WORKSPACE="$WORKSPACE_DIR/$VISIBLE_FOLDER_PROJECT/client" + VERSION_WORKSPACE="$PROJECT_WORKSPACE" + VISIBLE_FOLDER_DEVCONTAINER="$VISIBLE_FOLDER_PROJECT-$VISIBLE_FOLDER_CLIENT-$VISIBLE_FOLDER_SERVER" + PROJECT_DIR="$VISIBLE_FOLDER_PROJECT" +else + SERVER_WORKSPACE="$WORKSPACE_DIR/$VISIBLE_FOLDER_PROJECT/$VISIBLE_FOLDER_VERSION/server" + CLIENT_WORKSPACE="$WORKSPACE_DIR/$VISIBLE_FOLDER_PROJECT/$VISIBLE_FOLDER_VERSION/client" + VERSION_WORKSPACE="$PROJECT_WORKSPACE/$VISIBLE_FOLDER_VERSION" + VISIBLE_FOLDER_DEVCONTAINER="$VISIBLE_FOLDER_PROJECT-$VISIBLE_FOLDER_VERSION-$VISIBLE_FOLDER_CLIENT-$VISIBLE_FOLDER_SERVER" + PROJECT_DIR="$VISIBLE_FOLDER_PROJECT/$VISIBLE_FOLDER_VERSION" + +fi +echo "SERVER_WORKSPACE:$SERVER_WORKSPACE" +echo "CLIENT_WORKSPACE:$CLIENT_WORKSPACE" +echo "VISIBLE_FOLDER_DEVCONTAINER:$VISIBLE_FOLDER_DEVCONTAINER" +echo "PROJECT_WORKSPACE:$PROJECT_WORKSPACE" + + +echo "Workspace directory: $WORKSPACE_DIR" +echo "Server directory: $SERVER_WORKSPACE" +echo "Visible server folder: $VISIBLE_FOLDER_SERVER" +echo "Visible client folder: $VISIBLE_FOLDER_CLIENT" +echo "Visible project folder: $VISIBLE_FOLDER_PROJECT" +echo "Visible version: $VISIBLE_FOLDER_VERSION" +echo "Visible devcontainer: $DEVCONTAINER_WORKSPACE" +if [ ! -d "$SERVER_WORKSPACE" ]; then + echo "Error: Server directory not found at $SERVER_WORKSPACE" + exit 1 +fi + +if [ ! -d "$DEVCONTAINER_WORKSPACE" ]; then + echo "Error: .devcontainer directory not found at $DEVCONTAINER_WORKSPACE" + exit 1 +fi + +if [ -z "$VISIBLE_FOLDER_SERVER" ]; then + echo "Error: VISIBLE_FOLDER_SERVER is not set" + exit 1 +fi + +mkdir -p "$(dirname "$SETTINGS_FILE")" + +echo "{ + \"files.exclude\": {" > "$SETTINGS_FILE" + +first=true + +for dir in "$WORKSPACE_DIR"/*/ ; do + dir_name=$(basename "$dir") + if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_PROJECT" ]; then + if [ "$first" = true ] ; then + first=false + else + echo "," >> "$SETTINGS_FILE" + fi + echo -n " \"**/$dir_name\": true" >> "$SETTINGS_FILE" + fi +done + +if [ -n "$VISIBLE_FOLDER_VERSION" ]; then + for dir in "$PROJECT_WORKSPACE"/*/ ; do + dir_name=$(basename "$dir") + if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_VERSION" ]; then + if [ "$first" = true ] ; then + first=false + else + echo "," >> "$SETTINGS_FILE" + fi + echo -n " \"**/$VISIBLE_FOLDER_PROJECT/$dir_name\": true" >> "$SETTINGS_FILE" + fi + done +fi + +for dir in "$DEVCONTAINER_WORKSPACE"/*/ ; do + dir_name=$(basename "$dir") + if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_DEVCONTAINER" ]; then + if [ "$first" = true ] ; then + first=false + else + echo "," >> "$SETTINGS_FILE" + fi + echo -n " \"**/.devcontainer/$dir_name\": true" >> "$SETTINGS_FILE" + fi +done + +for dir in "$SERVER_WORKSPACE"/*/ ; do + dir_name=$(basename "$dir") + if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_SERVER" ]; then + if [ "$first" = true ] ; then + first=false + else + echo "," >> "$SETTINGS_FILE" + fi + echo -n " \"**/$PROJECT_DIR/server/$dir_name\": true" >> "$SETTINGS_FILE" + fi +done + +for dir in "$CLIENT_WORKSPACE"/*/ ; do + dir_name=$(basename "$dir") + if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_CLIENT" ]; then + if [ "$first" = true ] ; then + first=false + else + echo "," >> "$SETTINGS_FILE" + fi + echo -n " \"**/$PROJECT_DIR/client/$dir_name\": true" >> "$SETTINGS_FILE" + fi +done + +echo " + } +}" >> "$SETTINGS_FILE" + +echo "VS Code settings updated to show only $VISIBLE_FOLDER_SERVER and $VISIBLE_FOLDER_CLIENT folder in server directory." +echo "Contents of $SETTINGS_FILE:" +cat "$SETTINGS_FILE" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..ed606c67 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "env": { + "es2021": true + }, + "extends": ["eslint:recommended"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": {} +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 00000000..9ae8d9e3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,35 @@ +name: validate +on: + # run on push but only for the main branch + push: + branches: + - main + # run for every pull request + pull_request: {} +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: 🧹 Check code formatting with Prettier + run: > + find . -name package.json -maxdepth 2 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run format:check && cd - + done + + - name: 👕 Lint code with ESLint + run: > + find . -name package.json -maxdepth 3 -type f | while read -r file; do + directory=$(dirname "$file") + cd "$directory" && npm run lint && cd - + done diff --git a/.gitignore b/.gitignore index 5f9a1890..42d24751 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ typings/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ +__pycache__/ # Optional REPL history .node_repl_history @@ -99,3 +100,13 @@ dist # TernJS port file .tern-port + +# dotnet +*.sln + +# env +*.local + +#Codespace +.vscode/settings.json +.devcontainer/update_settings.sh diff --git a/LICENSE b/LICENSE index 261eeb9e..0699f06b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -175,24 +175,13 @@ END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] + Copyright 2022 PayPal Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index e8bbac74..98fbede3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ # PayPal Developer Docs Example Code -Examples from the official PayPal Developer Docs + +Examples from the official [PayPal Developer Docs](https://developer.paypal.com/). + +## Introduction and Overview + +This repository contains two directories: + +- [Standard integration](./standard-integration/) + - Set up standard payments on your checkout page for your buyers. +- [Advanced integration](./advanced-integration/) + - Build and customize a card payment form to accept debit and credit cards. + +**Not sure where to start?** Choose the [standard integration](./standard-integration/). + +### The PayPal JavaScript SDK + +These examples use the [PayPal JavaScript SDK](https://developer.paypal.com/sdk/js/) to display PayPal supported payment methods and provide a seamless checkout experience for your buyers. + +The SDK has several [configuration options](https://developer.paypal.com/sdk/js/configuration/) available. The examples in this repository provide the most minimal example possible to complete a successful transaction. + +## Know before you code + +### Setup a PayPal Account + +To get started with standard checkout, you'll need a developer, personal, or business account. + +[Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdeveloper%252Fapplications&intent=developer) + +You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to +make sandbox accounts. + +### Create an Application + +Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Have Node.js installed + +These examples will ask you to run commands like `npm install` and `npm start`. + +You'll need a version of node >= 16 which can be downloaded from the [Node.js website](https://nodejs.org/en/download/). + + +# Running on Codespaces +Follow below steps to use Codespaces. + +1) Click "New with options..." to open a page where you can choose the Dev container to run. +![image](https://github.com/user-attachments/assets/0d4bf202-0c94-42ec-aa2e-d8ccb6da9eb8) + +2) Choose the Dev container to run +![image](https://github.com/user-attachments/assets/b612467d-9fdc-4666-8dfa-0d99af6a2d39) + +3) Client ID and Client Secrets are required for running the application in codespace. +![image](https://github.com/user-attachments/assets/cbbc4521-aa43-403f-9243-e3c555e67f4a) + + +### Link to codespaces + +| Application | Codespaces Link | +| ---- | ---- | +| Advanced Integration v2 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v2%2Fdevcontainer.json)| +| Advanced Integration v1 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fadvanced-integration-v1%2Fdevcontainer.json)| +| Standard Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fstandard-integration%2Fdevcontainer.json)| +| Save Payment Method Integration | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/paypal-examples/docs-examples?devcontainer_path=.devcontainer%2Fsave-payment-method%2Fdevcontainer.json)| + +### Learn more + +You can read more about codespaces in the [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces). + +### Feedback + +* To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). +* To submit feedback, go to [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab. diff --git a/advanced-integration/.env b/advanced-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/advanced-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/advanced-integration/README.md b/advanced-integration/README.md index d282c7c3..ef1dd69d 100644 --- a/advanced-integration/README.md +++ b/advanced-integration/README.md @@ -1,9 +1,16 @@ -# Advanced Integration Example +# Advanced Checkout Integration Example + +This folder contains example code for a PayPal advanced Checkout integration using both the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +- [`v2`](v2/README.md) contains sample code for the current advanced Checkout integration. This includes guidance on using Card Fields. +- [`v1`](v1/README.md) contains sample code for the legacy advanced Checkout integration. Use `v2` for new integrations. ## Instructions -1. Add `CLIENT_ID` and `APP_SECRET` to the `.env` file +These instructions apply to the sample code for both `v2` and `v1`: + +1. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`. 2. Run `npm install` 3. Run `npm start` 4. Open http://localhost:8888 -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) \ No newline at end of file +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) diff --git a/advanced-integration/package.json b/advanced-integration/package.json deleted file mode 100644 index cc028060..00000000 --- a/advanced-integration/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@paypalcorp/advanced-integration", - "version": "1.0.0", - "description": "", - "main": "server.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" - }, - "author": "", - "license": "Apache-2.0", - "dependencies": { - "dotenv": "^16.0.0", - "ejs": "^3.1.6", - "express": "^4.17.3", - "node-fetch": "^3.2.1" - } -} diff --git a/advanced-integration/paypal-api.js b/advanced-integration/paypal-api.js deleted file mode 100644 index 082e2630..00000000 --- a/advanced-integration/paypal-api.js +++ /dev/null @@ -1,86 +0,0 @@ -import fetch from "node-fetch"; - -// set some important variables -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -// call the create order method -export async function createOrder() { - const purchaseAmount = "100.00"; // TODO: pull prices from a database - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: purchaseAmount, - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -// capture payment for an order -export async function capturePayment(orderId) { - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders/${orderId}/capture`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - }); - - return handleResponse(response); -} - -// generate access token -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -// generate client token -export async function generateClientToken() { - const accessToken = await generateAccessToken(); - const response = await fetch(`${base}/v1/identity/generate-token`, { - method: "post", - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Language": "en_US", - "Content-Type": "application/json", - }, - }); - console.log('response', response.status) - const jsonData = await handleResponse(response); - return jsonData.client_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/advanced-integration/public/app.js b/advanced-integration/public/app.js deleted file mode 100644 index 056efc84..00000000 --- a/advanced-integration/public/app.js +++ /dev/null @@ -1,144 +0,0 @@ -paypal - .Buttons({ - // Sets up the transaction when a payment button is clicked - createOrder: function (data, actions) { - return fetch("/api/orders", { - method: "post", - // use the "body" param to optionally pass additional order information - // like product ids or amount - }) - .then((response) => response.json()) - .then((order) => order.id); - }, - // Finalize the transaction after payer approval - onApprove: function (data, actions) { - return fetch(`/api/orders/${data.orderID}/capture`, { - method: "post", - }) - .then((response) => response.json()) - .then((orderData) => { - // Successful capture! For dev/demo purposes: - console.log( - "Capture result", - orderData, - JSON.stringify(orderData, null, 2) - ); - const transaction = orderData.purchase_units[0].payments.captures[0]; - alert(`Transaction ${transaction.status}: ${transaction.id} - - See console for all available details - `); - // When ready to go live, remove the alert and show a success message within this page. For example: - // var element = document.getElementById('paypal-button-container'); - // element.innerHTML = '

Thank you for your payment!

'; - // Or go to another URL: actions.redirect('thank_you.html'); - }); - }, - }) - .render("#paypal-button-container"); - -// If this returns false or the card fields aren't visible, see Step #1. -if (paypal.HostedFields.isEligible()) { - let orderId; - - // Renders card fields - paypal.HostedFields.render({ - // Call your server to set up the transaction - createOrder: () => { - return fetch("/api/orders", { - method: "post", - // use the "body" param to optionally pass additional order information like - // product ids or amount. - }) - .then((res) => res.json()) - .then((orderData) => { - orderId = orderData.id; // needed later to complete capture - return orderData.id; - }); - }, - styles: { - ".valid": { - color: "green", - }, - ".invalid": { - color: "red", - }, - }, - fields: { - number: { - selector: "#card-number", - placeholder: "4111 1111 1111 1111", - }, - cvv: { - selector: "#cvv", - placeholder: "123", - }, - expirationDate: { - selector: "#expiration-date", - placeholder: "MM/YY", - }, - }, - }).then((cardFields) => { - document.querySelector("#card-form").addEventListener("submit", (event) => { - event.preventDefault(); - cardFields - .submit({ - // Cardholder's first and last name - cardholderName: document.getElementById("card-holder-name").value, - // Billing Address - billingAddress: { - // Street address, line 1 - streetAddress: document.getElementById( - "card-billing-address-street" - ).value, - // Street address, line 2 (Ex: Unit, Apartment, etc.) - extendedAddress: document.getElementById( - "card-billing-address-unit" - ).value, - // State - region: document.getElementById("card-billing-address-state").value, - // City - locality: document.getElementById("card-billing-address-city") - .value, - // Postal Code - postalCode: document.getElementById("card-billing-address-zip") - .value, - // Country Code - countryCodeAlpha2: document.getElementById( - "card-billing-address-country" - ).value, - }, - }) - .then(() => { - fetch(`/api/orders/${orderId}/capture`, { - method: "post", - }) - .then((res) => res.json()) - .then((orderData) => { - // Two cases to handle: - // (1) Other non-recoverable errors -> Show a failure message - // (2) Successful transaction -> Show confirmation or thank you - // This example reads a v2/checkout/orders capture response, propagated from the server - // You could use a different API or structure for your 'orderData' - const errorDetail = - Array.isArray(orderData.details) && orderData.details[0]; - if (errorDetail) { - var msg = "Sorry, your transaction could not be processed."; - if (errorDetail.description) - msg += "\n\n" + errorDetail.description; - if (orderData.debug_id) msg += " (" + orderData.debug_id + ")"; - return alert(msg); // Show a failure message - } - // Show a success message or redirect - alert("Transaction completed!"); - }); - }) - .catch((err) => { - alert("Payment could not be captured! " + JSON.stringify(err)); - }); - }); - }); -} else { - // Hides card fields if the merchant isn't eligible - document.querySelector("#card-form").style = "display: none"; -} diff --git a/advanced-integration/server.js b/advanced-integration/server.js deleted file mode 100644 index e8069220..00000000 --- a/advanced-integration/server.js +++ /dev/null @@ -1,41 +0,0 @@ -import "dotenv/config"; -import express from "express"; -import * as paypal from "./paypal-api.js"; - -const app = express(); -app.set("view engine", "ejs"); -app.use(express.static("public")); - -// render checkout page with client id & unique client token -app.get("/", async (req, res) => { - const clientId = process.env.CLIENT_ID; - try { - const clientToken = await paypal.generateClientToken(); - res.render("checkout", { clientId, clientToken }); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// create order -app.post("/api/orders", async (req, res) => { - try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); - } -}); - -// capture payment -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; - try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.listen(8888); diff --git a/advanced-integration/v1/.gitignore b/advanced-integration/v1/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/advanced-integration/v1/.gitignore @@ -0,0 +1 @@ +.env diff --git a/advanced-integration/v1/README.md b/advanced-integration/v1/README.md new file mode 100644 index 00000000..152ef9ae --- /dev/null +++ b/advanced-integration/v1/README.md @@ -0,0 +1,14 @@ +# Advanced Integration Example + +This folder contains example code for [version 1](https://developer.paypal.com/docs/checkout/advanced/integrate/sdk/v1) of a PayPal advanced Checkout integration using the JavaScript SDK and Node.js to complete transactions with the PayPal REST API. + +> **Note:** Version 1 is a legacy integration. Use [version 2](https://developer.paypal.com/docs/checkout/advanced/integrate/) for new integrations. + +## Instructions + +1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create). +2. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET`. +3. Run `npm install`. +4. Run `npm start`. +5. Open http://localhost:8888. +6. 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). diff --git a/advanced-integration/v1/client/app.js b/advanced-integration/v1/client/app.js new file mode 100644 index 00000000..65f048d7 --- /dev/null +++ b/advanced-integration/v1/client/app.js @@ -0,0 +1,186 @@ +async function createOrderCallback() { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + console.error(error); + resultMessage(`Could not initiate PayPal Checkout...

${error}`); + } +} + +async function onApproveCallback(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + const errorDetail = orderData?.details?.[0]; + + // this actions.restart() behavior only applies to the Buttons component + if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if ( + errorDetail || + !transaction || + transaction.status === "DECLINED" + ) { + // (2) Other non-recoverable errors -> Show a failure message + let errorMessage; + if (transaction) { + errorMessage = `Transaction ${transaction.status}: ${transaction.id}`; + } else if (errorDetail) { + errorMessage = `${errorDetail.description} (${orderData.debug_id})`; + } else { + errorMessage = JSON.stringify(orderData); + } + + throw new Error(errorMessage); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + resultMessage( + `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, + ); + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2), + ); + } + } catch (error) { + console.error(error); + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + } +} + +window.paypal + .Buttons({ + createOrder: createOrderCallback, + onApprove: onApproveCallback, + }) + .render("#paypal-button-container"); + +// Example function to show a result to the user. Your site's UI library can be used instead. +function resultMessage(message) { + const container = document.querySelector("#result-message"); + container.innerHTML = message; +} + +// If this returns false or the card fields aren't visible, see Step #1. +if (window.paypal.HostedFields.isEligible()) { + // Renders card fields + window.paypal.HostedFields.render({ + // Call your server to set up the transaction + createOrder: createOrderCallback, + styles: { + ".valid": { + color: "green", + }, + ".invalid": { + color: "red", + }, + }, + fields: { + number: { + selector: "#card-number", + placeholder: "4111 1111 1111 1111", + }, + cvv: { + selector: "#cvv", + placeholder: "123", + }, + expirationDate: { + selector: "#expiration-date", + placeholder: "MM/YY", + }, + }, + }).then((cardFields) => { + document.querySelector("#card-form").addEventListener("submit", (event) => { + event.preventDefault(); + cardFields + .submit({ + // Cardholder's first and last name + cardholderName: document.getElementById("card-holder-name").value, + // Billing Address + billingAddress: { + // Street address, line 1 + streetAddress: document.getElementById( + "card-billing-address-street", + ).value, + // Street address, line 2 (Ex: Unit, Apartment, etc.) + extendedAddress: document.getElementById( + "card-billing-address-unit", + ).value, + // State + region: document.getElementById("card-billing-address-state").value, + // City + locality: document.getElementById("card-billing-address-city") + .value, + // Postal Code + postalCode: document.getElementById("card-billing-address-zip") + .value, + // Country Code + countryCodeAlpha2: document.getElementById( + "card-billing-address-country", + ).value, + }, + }) + .then((data) => { + return onApproveCallback(data); + }) + .catch((orderData) => { + resultMessage( + `Sorry, your transaction could not be processed...

${JSON.stringify( + orderData, + )}`, + ); + }); + }); + }); +} else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; +} diff --git a/advanced-integration/v1/env.example b/advanced-integration/v1/env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/advanced-integration/v1/env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE diff --git a/advanced-integration/v1/package.json b/advanced-integration/v1/package.json new file mode 100644 index 00000000..0e93024f --- /dev/null +++ b/advanced-integration/v1/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-advanced-integration", + "description": "Sample Node.js web app to integrate PayPal Advanced Checkout for online payments", + "version": "1.0.0", + "main": "server/server.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "nodemon server/server.js", + "format": "npx prettier --write **/*.{js,md}", + "format:check": "npx prettier --check **/*.{js,md}", + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" + }, + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/advanced-integration/v1/server/server.js b/advanced-integration/v1/server/server.js new file mode 100644 index 00000000..a7d84407 --- /dev/null +++ b/advanced-integration/v1/server/server.js @@ -0,0 +1,178 @@ +import express from "express"; +import fetch from "node-fetch"; +import "dotenv/config"; + +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env; +const base = "https://api-m.sandbox.paypal.com"; +const app = express(); +app.set("view engine", "ejs"); +app.set("views", "./server/views"); +app.use(express.static("client")); + +// parse post params sent in body in json format +app.use(express.json()); + +/** + * Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs. + * @see https://developer.paypal.com/api/rest/authentication/ + */ +const generateAccessToken = async () => { + try { + if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { + throw new Error("MISSING_API_CREDENTIALS"); + } + const auth = Buffer.from( + PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, + ).toString("base64"); + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: "grant_type=client_credentials", + headers: { + Authorization: `Basic ${auth}`, + }, + }); + + const data = await response.json(); + return data.access_token; + } catch (error) { + console.error("Failed to generate Access Token:", error); + } +}; + +/** + * Generate a client token for rendering the hosted card fields. + * @see https://developer.paypal.com/docs/checkout/advanced/integrate/#link-integratebackend + */ +const generateClientToken = async () => { + const accessToken = await generateAccessToken(); + const url = `${base}/v1/identity/generate-token`; + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Language": "en_US", + "Content-Type": "application/json", + }, + }); + + return handleResponse(response); +}; + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +const createOrder = async (cart) => { + // use the cart information passed from the front-end to calculate the purchase unit details + console.log( + "shopping cart information passed from the frontend createOrder() callback:", + cart, + ); + + const accessToken = await generateAccessToken(); + const url = `${base}/v2/checkout/orders`; + const payload = { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "100.00", + }, + }, + ], + }; + + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation: + // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' + }, + method: "POST", + body: JSON.stringify(payload), + }); + + return handleResponse(response); +}; + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +const captureOrder = async (orderID) => { + const accessToken = await generateAccessToken(); + const url = `${base}/v2/checkout/orders/${orderID}/capture`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation: + // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' + }, + }); + + return handleResponse(response); +}; + +async function handleResponse(response) { + try { + const jsonResponse = await response.json(); + return { + jsonResponse, + httpStatusCode: response.status, + }; + } catch (err) { + const errorMessage = await response.text(); + throw new Error(errorMessage); + } +} + +// render checkout page with client id & unique client token +app.get("/", async (req, res) => { + try { + const { jsonResponse } = await generateClientToken(); + res.render("checkout", { + clientId: PAYPAL_CLIENT_ID, + clientToken: jsonResponse.client_token, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +app.post("/api/orders", async (req, res) => { + try { + // use the cart information passed from the front-end to calculate the order amount detals + const { cart } = req.body; + const { jsonResponse, httpStatusCode } = await createOrder(cart); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to create order." }); + } +}); + +app.post("/api/orders/:orderID/capture", async (req, res) => { + try { + const { orderID } = req.params; + const { jsonResponse, httpStatusCode } = await captureOrder(orderID); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/advanced-integration/v1/server/views/checkout.ejs b/advanced-integration/v1/server/views/checkout.ejs new file mode 100644 index 00000000..85cd7085 --- /dev/null +++ b/advanced-integration/v1/server/views/checkout.ejs @@ -0,0 +1,104 @@ + + + + + + PayPal JS SDK Advanced Integration + + + + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+

+
+ + + diff --git a/advanced-integration/views/checkout.ejs b/advanced-integration/v1/views/checkout.ejs similarity index 100% rename from advanced-integration/views/checkout.ejs rename to advanced-integration/v1/views/checkout.ejs diff --git a/advanced-integration/v2/client/html/.env.example b/advanced-integration/v2/client/html/.env.example new file mode 100644 index 00000000..dbfed9bd --- /dev/null +++ b/advanced-integration/v2/client/html/.env.example @@ -0,0 +1,4 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE diff --git a/advanced-integration/v2/client/html/.gitignore b/advanced-integration/v2/client/html/.gitignore new file mode 100644 index 00000000..ed648a05 --- /dev/null +++ b/advanced-integration/v2/client/html/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.local \ No newline at end of file diff --git a/advanced-integration/v2/client/html/README.md b/advanced-integration/v2/client/html/README.md new file mode 100644 index 00000000..08187da8 --- /dev/null +++ b/advanced-integration/v2/client/html/README.md @@ -0,0 +1,74 @@ +# Advanced Integration with PayPal : HTML/JS + +## Getting Started + +This guide will walk you through setting up and running the HTML/JS Advanced Integration locally. + +### Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Installation + +```bash +npm install +``` + +### Configuration + +1. Environmental Variables (.env) + + - Rename the .env.example file to .env + - Update the following keys with their actual values - + + ```bash + PAYPAL_CLIENT_ID= + ``` + +2. Connecting the client and server (vite.config.js) + + - Open vite.config.js in the root directory. + - Locate the proxy configuration object. + - Update the proxy key to match the server's address and port. For example: + + ```js + export default defineConfig({ + + server: { + proxy: { + "/api": { + target: "http://localhost:8080", // Replace with your server URL + changeOrigin: true, + }, + }, + }, + }); + ``` + +3. Starting the development server + + - **Start the server**: Follow the instructions in the server's README to start it. Typically, this involves running npm run start or a similar command in the server directory. + + - **Start the client**: + + ```bash + npm run start + ``` + + This will start the development server, and you should be able to access the Advanced Checkout Page in your browser at `http://localhost:3000` (or the port specfied in the terminal output). + +### Additional Notes + +- **Server Setup**: Make sure you have the server up and running before starting the client. +- **Environment Variables**: Carefully configure the environment variables in the .env file to match your setup. +- **Proxy Configuration**: The proxy setting in vite.config.js is crucial for routing API requests from the client to the server during development. diff --git a/advanced-integration/v2/client/html/package.json b/advanced-integration/v2/client/html/package.json new file mode 100644 index 00000000..c14f152f --- /dev/null +++ b/advanced-integration/v2/client/html/package.json @@ -0,0 +1,17 @@ +{ + "name": "paypal-advanced-integration-frontend-html", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview", + "start": "vite", + "format": "npx prettier --write **/*.{js,md}", + "format:check": "npx prettier --check **/*.{js,md}" + }, + "devDependencies": { + "dotenv": "^16.4.5", + "vite": "^5.4.2" + } +} diff --git a/advanced-integration/v2/client/html/src/app.js b/advanced-integration/v2/client/html/src/app.js new file mode 100644 index 00000000..02cd182a --- /dev/null +++ b/advanced-integration/v2/client/html/src/app.js @@ -0,0 +1,167 @@ +async function createOrderCallback() { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + console.error(error); + resultMessage(`Could not initiate PayPal Checkout...

${error}`); + } + } + + async function onApproveCallback(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + const errorDetail = orderData?.details?.[0]; + + // this actions.restart() behavior only applies to the Buttons component + if (errorDetail?.issue === "INSTRUMENT_DECLINED" && !data.card && actions) { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if ( + errorDetail || + !transaction || + transaction.status === "DECLINED" + ) { + // (2) Other non-recoverable errors -> Show a failure message + let errorMessage; + if (transaction) { + errorMessage = `Transaction ${transaction.status}: ${transaction.id}`; + } else if (errorDetail) { + errorMessage = `${errorDetail.description} (${orderData.debug_id})`; + } else { + errorMessage = JSON.stringify(orderData); + } + + throw new Error(errorMessage); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + resultMessage( + `Transaction ${transaction.status}: ${transaction.id}

See console for all available details`, + ); + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2), + ); + } + } catch (error) { + console.error(error); + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + } + } + + window.paypal + .Buttons({ + createOrder: createOrderCallback, + onApprove: onApproveCallback, + }) + .render("#paypal-button-container"); + + const cardField = window.paypal.CardFields({ + createOrder: createOrderCallback, + onApprove: onApproveCallback, + }); + + // Render each field after checking for eligibility + if (cardField.isEligible()) { + const nameField = cardField.NameField(); + nameField.render("#card-name-field-container"); + + const numberField = cardField.NumberField(); + numberField.render("#card-number-field-container"); + + const cvvField = cardField.CVVField(); + cvvField.render("#card-cvv-field-container"); + + const expiryField = cardField.ExpiryField(); + expiryField.render("#card-expiry-field-container"); + + // Add click listener to submit button and call the submit function on the CardField component + document + .getElementById("card-field-submit-button") + .addEventListener("click", () => { + cardField + .submit({ + // From your billing address fields + billingAddress: { + addressLine1: document.getElementById("card-billing-address-line-1") + .value, + addressLine2: document.getElementById("card-billing-address-line-2") + .value, + adminArea1: document.getElementById( + "card-billing-address-admin-area-line-1", + ).value, + adminArea2: document.getElementById( + "card-billing-address-admin-area-line-2", + ).value, + countryCode: document.getElementById( + "card-billing-address-country-code", + ).value, + postalCode: document.getElementById( + "card-billing-address-postal-code", + ).value, + }, + }) + .catch((error) => { + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + }); + }); + } else { + // Hides card fields if the merchant isn't eligible + document.querySelector("#card-form").style = "display: none"; + } + + // Example function to show a result to the user. Your site's UI library can be used instead. + function resultMessage(message) { + const container = document.querySelector("#result-message"); + container.innerHTML = message; + } + \ No newline at end of file diff --git a/advanced-integration/v2/client/html/src/index.html b/advanced-integration/v2/client/html/src/index.html new file mode 100644 index 00000000..328d4da4 --- /dev/null +++ b/advanced-integration/v2/client/html/src/index.html @@ -0,0 +1,86 @@ + + + + + + + PayPal JS SDK Advanced Integration - Checkout Flow + + +
+ +
+
+
+
+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+

+ +
+

+ + + + diff --git a/advanced-integration/v2/client/html/vite.config.js b/advanced-integration/v2/client/html/vite.config.js new file mode 100644 index 00000000..0a14da0e --- /dev/null +++ b/advanced-integration/v2/client/html/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + envDir: "../", + envPrefix: "PAYPAL", + root: "src", + server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + }, + }, +}) \ No newline at end of file diff --git a/advanced-integration/v2/client/react/.env.example b/advanced-integration/v2/client/react/.env.example new file mode 100644 index 00000000..9ce40285 --- /dev/null +++ b/advanced-integration/v2/client/react/.env.example @@ -0,0 +1,4 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID \ No newline at end of file diff --git a/advanced-integration/v2/client/react/.gitignore b/advanced-integration/v2/client/react/.gitignore new file mode 100644 index 00000000..ed648a05 --- /dev/null +++ b/advanced-integration/v2/client/react/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.local \ No newline at end of file diff --git a/advanced-integration/v2/client/react/README.md b/advanced-integration/v2/client/react/README.md new file mode 100644 index 00000000..107c6b55 --- /dev/null +++ b/advanced-integration/v2/client/react/README.md @@ -0,0 +1,74 @@ +# Advanced Integration with PayPal : React + +## Getting Started + +This guide will walk you through setting up and running the React Advanced Integration locally. + +### Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Installation + +```bash +npm install +``` + +### Configuration + +1. Environmental Variables (.env) + + - Rename the .env.example file to .env + - Update the following keys with their actual values - + + ```bash + PAYPAL_CLIENT_ID= + ``` + +2. Connecting the client and server (vite.config.js) + + - Open vite.config.js in the root directory. + - Locate the proxy configuration object. + - Update the proxy key to match the server's address and port. For example: + + ```js + export default defineConfig({ + + server: { + proxy: { + "/api": { + target: "http://localhost:8080", // Replace with your server URL + changeOrigin: true, + }, + }, + }, + }); + ``` + +3. Starting the development server + + - **Start the server**: Follow the instructions in the server's README to start it. Typically, this involves running npm run start or a similar command in the server directory. + + - **Start the client**: + + ```bash + npm run start + ``` + + This will start the development server, and you should be able to access the Advanced Checkout Page in your browser at `http://localhost:3000` (or the port specfied in the terminal output). + +### Additional Notes + +- **Server Setup**: Make sure you have the server up and running before starting the client. +- **Environment Variables**: Carefully configure the environment variables in the .env file to match your setup. +- **Proxy Configuration**: The proxy setting in vite.config.js is crucial for routing API requests from the client to the server during development. diff --git a/advanced-integration/v2/client/react/package.json b/advanced-integration/v2/client/react/package.json new file mode 100644 index 00000000..b61d3e00 --- /dev/null +++ b/advanced-integration/v2/client/react/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-advanced-integration-frontend-react", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@paypal/react-paypal-js": "^8.6.0", + "dotenv": "^16.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "scripts": { + "client-dev": "vite", + "client-build": "vite build", + "client-preview": "vite preview", + "start": "npm run client-dev", + "format": "npx prettier --write **/*.{js,jsx,md}", + "format:check": "npx prettier --check **/*.{js,jsx,md}" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.2" + } +} diff --git a/advanced-integration/v2/client/react/src/App.jsx b/advanced-integration/v2/client/react/src/App.jsx new file mode 100644 index 00000000..fc670070 --- /dev/null +++ b/advanced-integration/v2/client/react/src/App.jsx @@ -0,0 +1,246 @@ +import React, { useState } from "react"; +import { + PayPalScriptProvider, + usePayPalCardFields, + PayPalCardFieldsProvider, + PayPalButtons, + PayPalCardFieldsForm, +} from "@paypal/react-paypal-js"; + +export default function App() { + const [isPaying, setIsPaying] = useState(false); + const [message, setMessage] = useState(""); + const initialOptions = { + "client-id": import.meta.env.PAYPAL_CLIENT_ID, + "enable-funding": "venmo", + "buyer-country": "US", + currency: "USD", + components: "buttons,card-fields", + }; + + const [billingAddress, setBillingAddress] = useState({ + addressLine1: "", + addressLine2: "", + adminArea1: "", + adminArea2: "", + countryCode: "", + postalCode: "", + }); + + function handleBillingAddressChange(field, value) { + setBillingAddress((prev) => ({ + ...prev, + [field]: value, + })); + } + + async function createOrder() { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + console.error(error); + return `Could not initiate PayPal Checkout...${error}`; + } + } + + async function onApprove(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + const errorDetail = orderData?.details?.[0]; + + if (errorDetail || !transaction || transaction.status === "DECLINED") { + // (2) Other non-recoverable errors -> Show a failure message + let errorMessage; + if (transaction) { + errorMessage = `Transaction ${transaction.status}: ${transaction.id}`; + } else if (errorDetail) { + errorMessage = `${errorDetail.description} (${orderData.debug_id})`; + } else { + errorMessage = JSON.stringify(orderData); + } + + throw new Error(errorMessage); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2) + ); + return `Transaction ${transaction.status}: ${transaction.id}. See console for all available details`; + } + } catch (error) { + return `Sorry, your transaction could not be processed...${error}`; + } + } + + function onError(error) { + // Do something with the error from the SDK + } + + return ( +
+ + setMessage(await onApprove(data))} + onError={onError} + style={{ + shape: "rect", + layout: "vertical", + color: "gold", + label: "paypal", + }} + /> + + setMessage(await onApprove(data))} + > + + + handleBillingAddressChange("addressLine1", e.target.value) + } + /> + + handleBillingAddressChange("addressLine2", e.target.value) + } + /> + + handleBillingAddressChange("adminArea1", e.target.value) + } + /> + + handleBillingAddressChange("adminArea2", e.target.value) + } + /> + + handleBillingAddressChange("countryCode", e.target.value) + } + /> + + handleBillingAddressChange("postalCode", e.target.value) + } + /> + {/* Custom client component to handle card fields submission */} + + + + +
+ ); +} + +const SubmitPayment = ({ isPaying, setIsPaying, billingAddress }) => { + const { cardFieldsForm, fields } = usePayPalCardFields(); + + const handleClick = async () => { + if (!cardFieldsForm) { + const childErrorMessage = + "Unable to find any child components in the "; + + throw new Error(childErrorMessage); + } + const formState = await cardFieldsForm.getState(); + + if (!formState.isFormValid) { + return alert("The payment form is invalid"); + } + setIsPaying(true); + + cardFieldsForm.submit({ billingAddress }).finally((err) => { + setIsPaying(false); + }); + }; + + return ( + + ); +}; + +const Message = ({ content }) => { + return

{content}

; +}; diff --git a/advanced-integration/v2/client/react/src/index.html b/advanced-integration/v2/client/react/src/index.html new file mode 100644 index 00000000..4330e684 --- /dev/null +++ b/advanced-integration/v2/client/react/src/index.html @@ -0,0 +1,17 @@ + + + + + + React PayPal JS SDK Advanced Integration + + + +
+ + + diff --git a/advanced-integration/v2/client/react/src/main.jsx b/advanced-integration/v2/client/react/src/main.jsx new file mode 100644 index 00000000..569fdf2f --- /dev/null +++ b/advanced-integration/v2/client/react/src/main.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/advanced-integration/v2/client/react/vite.config.js b/advanced-integration/v2/client/react/vite.config.js new file mode 100644 index 00000000..274f8cf5 --- /dev/null +++ b/advanced-integration/v2/client/react/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + root: "src", + envDir: "../", + envPrefix: "PAYPAL", + server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + }, + }, +}) \ No newline at end of file diff --git a/advanced-integration/v2/server/dotnet/.gitignore b/advanced-integration/v2/server/dotnet/.gitignore new file mode 100644 index 00000000..cbbd0b5c --- /dev/null +++ b/advanced-integration/v2/server/dotnet/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/advanced-integration/v2/server/dotnet/PayPalAdvancedIntegration.csproj b/advanced-integration/v2/server/dotnet/PayPalAdvancedIntegration.csproj new file mode 100644 index 00000000..c086ae30 --- /dev/null +++ b/advanced-integration/v2/server/dotnet/PayPalAdvancedIntegration.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + PayPalAdvancedIntegration + + + + + + + \ No newline at end of file diff --git a/advanced-integration/v2/server/dotnet/README.md b/advanced-integration/v2/server/dotnet/README.md new file mode 100644 index 00000000..6170999e --- /dev/null +++ b/advanced-integration/v2/server/dotnet/README.md @@ -0,0 +1,35 @@ +# Advanced Integration .NET Sample + +PayPal Advanced Integration sample in .NET + +## Running the sample + +1. **Add your API credentials to the environment:** + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. **Build the server** + + ```bash + dotnet restore + ``` + +3. **Run the server** + + ```bash + dotnet run + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/advanced-integration/v2/server/dotnet/Server.cs b/advanced-integration/v2/server/dotnet/Server.cs new file mode 100644 index 00000000..2b4a1c4f --- /dev/null +++ b/advanced-integration/v2/server/dotnet/Server.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PaypalServerSdk.Standard; +using PaypalServerSdk.Standard.Authentication; +using PaypalServerSdk.Standard.Controllers; +using PaypalServerSdk.Standard.Http.Response; +using PaypalServerSdk.Standard.Models; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; + +namespace PayPalAdvancedIntegration; + +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseUrls("http://localhost:8080"); + webBuilder.UseStartup(); + }); +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().AddNewtonsoftJson(); + services.AddHttpClient(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseRouting(); + app.UseStaticFiles(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} + +[ApiController] +public class CheckoutController : Controller +{ + private readonly OrdersController _ordersController; + private readonly PaymentsController _paymentsController; + + private IConfiguration _configuration { get; } + private string _paypalClientId + { + get { return System.Environment.GetEnvironmentVariable("PAYPAL_CLIENT_ID"); } + } + private string _paypalClientSecret + { + get { return System.Environment.GetEnvironmentVariable("PAYPAL_CLIENT_SECRET"); } + } + + private readonly ILogger _logger; + + public CheckoutController(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + + // Initialize the PayPal SDK client + PaypalServerSdkClient client = new PaypalServerSdkClient.Builder() + .Environment(PaypalServerSdk.Standard.Environment.Sandbox) + .ClientCredentialsAuth( + new ClientCredentialsAuthModel.Builder(_paypalClientId, _paypalClientSecret).Build() + ) + .LoggingConfig(config => + config + .LogLevel(LogLevel.Information) + .RequestConfig(reqConfig => reqConfig.Body(true)) + .ResponseConfig(respConfig => respConfig.Headers(true)) + ) + .Build(); + + _ordersController = client.OrdersController; + _paymentsController = client.PaymentsController; + } + + [HttpPost("api/orders")] + public async Task CreateOrder([FromBody] dynamic cart) + { + try + { + var result = await _CreateOrder(cart); + return StatusCode((int)result.StatusCode, result.Data); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to create order:", ex); + return StatusCode(500, new { error = "Failed to create order." }); + } + } + + private async Task _CreateOrder(dynamic cart) + { + CheckoutPaymentIntent intent = (CheckoutPaymentIntent) + Enum.Parse(typeof(CheckoutPaymentIntent), "CAPTURE", true); + + CreateOrderInput createOrderInput = new CreateOrderInput + { + Body = new OrderRequest + { + Intent = intent, + PurchaseUnits = new List + { + new PurchaseUnitRequest + { + Amount = new AmountWithBreakdown { CurrencyCode = "USD", MValue = "100", }, + }, + }, + }, + }; + + ApiResponse result = await _ordersController.CreateOrderAsync(createOrderInput); + return result; + } + + [HttpPost("api/orders/{orderID}/capture")] + public async Task CaptureOrder(string orderID) + { + try + { + var result = await _CaptureOrder(orderID); + return StatusCode((int)result.StatusCode, result.Data); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to capture order:", ex); + return StatusCode(500, new { error = "Failed to capture order." }); + } + } + + private async Task _CaptureOrder(string orderID) + { + CaptureOrderInput ordersCaptureInput = new CaptureOrderInput { Id = orderID, }; + + ApiResponse result = await _ordersController.CaptureOrderAsync(ordersCaptureInput); + + return result; + } +} diff --git a/advanced-integration/v2/server/java/.gitignore b/advanced-integration/v2/server/java/.gitignore new file mode 100644 index 00000000..9f970225 --- /dev/null +++ b/advanced-integration/v2/server/java/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/advanced-integration/v2/server/java/.tool-versions b/advanced-integration/v2/server/java/.tool-versions new file mode 100644 index 00000000..edcd84e6 --- /dev/null +++ b/advanced-integration/v2/server/java/.tool-versions @@ -0,0 +1 @@ +java zulu-21.36.19 diff --git a/advanced-integration/v2/server/java/README.md b/advanced-integration/v2/server/java/README.md new file mode 100644 index 00000000..d88ad4a2 --- /dev/null +++ b/advanced-integration/v2/server/java/README.md @@ -0,0 +1,35 @@ +# Advanced Integration Java Sample + +PayPal Advanced Integration sample in Java + +## Running the sample + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Build the server + + ```bash + mvn clean install + ``` + +3. Run the server + + ```bash + mvn spring-boot:run + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/advanced-integration/v2/server/java/pom.xml b/advanced-integration/v2/server/java/pom.xml new file mode 100644 index 00000000..ef04e9c0 --- /dev/null +++ b/advanced-integration/v2/server/java/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.3 + + + com.paypal.sample + advanced-integration + 0.0.1-SNAPSHOT + PayPal Advanced Integration + Sample Java demo application for PayPal JS SDK Advanced Integration + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-thymeleaf + 3.3.3 + + + org.springframework.boot + spring-boot-starter-web + 3.3.3 + + + + com.paypal.sdk + + + paypal-server-sdk + + + 1.0.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.3.3 + + + + + diff --git a/advanced-integration/v2/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java b/advanced-integration/v2/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java new file mode 100644 index 00000000..49e6dd47 --- /dev/null +++ b/advanced-integration/v2/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java @@ -0,0 +1,141 @@ +package com.paypal.sample; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.client.RestTemplate; + +import com.paypal.sdk.Environment; +import com.paypal.sdk.PaypalServerSdkClient; +import com.paypal.sdk.authentication.ClientCredentialsAuthModel; +import com.paypal.sdk.controllers.OrdersController; +import com.paypal.sdk.exceptions.ApiException; +import com.paypal.sdk.http.response.ApiResponse; +import com.paypal.sdk.models.AmountWithBreakdown; +import com.paypal.sdk.models.CheckoutPaymentIntent; +import com.paypal.sdk.models.Order; +import com.paypal.sdk.models.OrderRequest; +import com.paypal.sdk.models.CaptureOrderInput; +import com.paypal.sdk.models.CreateOrderInput; +import com.paypal.sdk.models.PurchaseUnitRequest; +import java.util.Arrays; +import org.slf4j.event.Level; + +import java.io.IOException; +import java.util.Map; + +@SpringBootApplication +public class SampleAppApplication { + + @Value("${PAYPAL_CLIENT_ID}") + private String PAYPAL_CLIENT_ID; + + @Value("${PAYPAL_CLIENT_SECRET}") + private String PAYPAL_CLIENT_SECRET; + + public static void main(String[] args) { + SpringApplication.run(SampleAppApplication.class, args); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public PaypalServerSdkClient paypalClient() { + return new PaypalServerSdkClient.Builder() + .loggingConfig(builder -> builder + .level(Level.DEBUG) + .requestConfig(logConfigBuilder -> logConfigBuilder.body(true)) + .responseConfig(logConfigBuilder -> logConfigBuilder.headers(true))) + .httpClientConfig(configBuilder -> configBuilder + .timeout(0)) + .environment(Environment.SANDBOX) + .clientCredentialsAuth(new ClientCredentialsAuthModel.Builder( + PAYPAL_CLIENT_ID, + PAYPAL_CLIENT_SECRET) + .build()) + .build(); + } + + + @Controller + @RequestMapping("/") + public class CheckoutController { + + private final ObjectMapper objectMapper; + private final PaypalServerSdkClient client; + + public CheckoutController(ObjectMapper objectMapper, PaypalServerSdkClient client) { + this.objectMapper = objectMapper; + this.client = client; + } + + @PostMapping("/api/orders") + public ResponseEntity createOrder(@RequestBody Map request) { + try { + String cart = objectMapper.writeValueAsString(request.get("cart")); + Order response = createOrder(cart); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/api/orders/{orderID}/capture") + public ResponseEntity captureOrder(@PathVariable String orderID) { + try { + Order response = captureOrders(orderID); + return new ResponseEntity(response, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private Order createOrder(String cart) throws IOException, ApiException { + + CreateOrderInput createOrderInput = new CreateOrderInput.Builder( + null, + new OrderRequest.Builder( + CheckoutPaymentIntent.CAPTURE, + Arrays.asList( + new PurchaseUnitRequest.Builder( + new AmountWithBreakdown.Builder( + "USD", + "100.00") + .build()) + .build())) + .build()) + .build(); + + OrdersController ordersController = client.getOrdersController(); + + ApiResponse apiResponse = ordersController.createOrder(createOrderInput); + + return apiResponse.getResult(); + } + + private Order captureOrders(String orderID) throws IOException, ApiException { + CaptureOrderInput ordersCaptureInput = new CaptureOrderInput.Builder( + orderID, + null) + .build(); + OrdersController ordersController = client.getOrdersController(); + ApiResponse apiResponse = ordersController.captureOrder(ordersCaptureInput); + return apiResponse.getResult(); + } + } +} diff --git a/advanced-integration/v2/server/java/src/main/resources/application.properties b/advanced-integration/v2/server/java/src/main/resources/application.properties new file mode 100644 index 00000000..103551b3 --- /dev/null +++ b/advanced-integration/v2/server/java/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=v2-java +PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID} +PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET} diff --git a/advanced-integration/v2/server/node/.env.example b/advanced-integration/v2/server/node/.env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/advanced-integration/v2/server/node/.env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE diff --git a/advanced-integration/v2/server/node/README.md b/advanced-integration/v2/server/node/README.md new file mode 100644 index 00000000..e2c1f93e --- /dev/null +++ b/advanced-integration/v2/server/node/README.md @@ -0,0 +1,35 @@ +# Advanced Integration Node.js Sample + +PayPal Advanced Integration sample in Node.js + +## Running the sample + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Install the packages + + ```bash + npm install + ``` + +3. Run the server + + ```bash + npm run start + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/advanced-integration/v2/server/node/package.json b/advanced-integration/v2/server/node/package.json new file mode 100644 index 00000000..120f5b7d --- /dev/null +++ b/advanced-integration/v2/server/node/package.json @@ -0,0 +1,23 @@ +{ + "name": "paypal-advanced-integration-backend-node", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@paypal/paypal-server-sdk": "^1.0.0", + "body-parser": "^1.20.3", + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "scripts": { + "server-dev": "nodemon server.js", + "start": "npm run server-dev", + "prod": "node server.js", + "format": "npx prettier --write **/*.{js,jsx,md}", + "format:check": "npx prettier --check **/*.{js,jsx,md}" + }, + "devDependencies": { + "concurrently": "^8.2.1", + "nodemon": "^3.0.1" + } +} diff --git a/advanced-integration/v2/server/node/server.js b/advanced-integration/v2/server/node/server.js new file mode 100644 index 00000000..3633923a --- /dev/null +++ b/advanced-integration/v2/server/node/server.js @@ -0,0 +1,129 @@ +import express from "express"; +import "dotenv/config"; +import { + ApiError, + CheckoutPaymentIntent, + Client, + Environment, + LogLevel, + OrdersController, +} from "@paypal/paypal-server-sdk"; +import bodyParser from "body-parser"; + +const app = express(); +app.use(bodyParser.json()); + +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8080 } = process.env; + +const client = new Client({ + clientCredentialsAuthCredentials: { + oAuthClientId: PAYPAL_CLIENT_ID, + oAuthClientSecret: PAYPAL_CLIENT_SECRET, + }, + timeout: 0, + environment: Environment.Sandbox, + logging: { + logLevel: LogLevel.Info, + logRequest: { + logBody: true, + }, + logResponse: { + logHeaders: true, + }, + }, +}); + +const ordersController = new OrdersController(client); + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +const createOrder = async (cart) => { + const collect = { + body: { + intent: CheckoutPaymentIntent.Capture, + purchaseUnits: [ + { + amount: { + currencyCode: "USD", + value: "100.00", + }, + }, + ], + }, + prefer: "return=minimal", + }; + + try { + const { body, ...httpResponse } = await ordersController.createOrder( + collect + ); + // Get more response info... + // const { statusCode, headers } = httpResponse; + return { + jsonResponse: JSON.parse(body), + httpStatusCode: httpResponse.statusCode, + }; + } catch (error) { + if (error instanceof ApiError) { + // const { statusCode, headers } = error; + throw new Error(error.message); + } + } +}; + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +const captureOrder = async (orderID) => { + const collect = { + id: orderID, + prefer: "return=minimal", + }; + + try { + const { body, ...httpResponse } = await ordersController.captureOrder( + collect + ); + // Get more response info... + // const { statusCode, headers } = httpResponse; + return { + jsonResponse: JSON.parse(body), + httpStatusCode: httpResponse.statusCode, + }; + } catch (error) { + if (error instanceof ApiError) { + // const { statusCode, headers } = error; + throw new Error(error.message); + } + } +}; + +app.post("/api/orders", async (req, res) => { + try { + // use the cart information passed from the front-end to calculate the order amount detals + const { cart } = req.body; + const { jsonResponse, httpStatusCode } = await createOrder(cart); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to create order." }); + } +}); + +app.post("/api/orders/:orderID/capture", async (req, res) => { + try { + const { orderID } = req.params; + const { jsonResponse, httpStatusCode } = await captureOrder(orderID); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/advanced-integration/v2/server/php/.gitignore b/advanced-integration/v2/server/php/.gitignore new file mode 100644 index 00000000..a725465a --- /dev/null +++ b/advanced-integration/v2/server/php/.gitignore @@ -0,0 +1 @@ +vendor/ \ No newline at end of file diff --git a/advanced-integration/v2/server/php/README.md b/advanced-integration/v2/server/php/README.md new file mode 100644 index 00000000..2bcecf8d --- /dev/null +++ b/advanced-integration/v2/server/php/README.md @@ -0,0 +1,63 @@ +# Advanced Integration Sample Application - PHP + +This sample app demonstrates how to integrate with ACDC using PayPal's REST APIs. + +## Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +## How to Run Locally + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Follow the below instructions to setup & run server. + +## Install the Composer + +We'll be using Composer (https://getcomposer.org/) for dependency management. To install Composer on a Mac, run the following command in the terminal: + +```bash +brew install composer +``` + +Composer can be downloaded for Windows from this link: https://getcomposer.org/download/. + +## To install the dependencies + +```bash +composer install +``` + +## To run the application in development, you can run this command + +```bash +composer start +``` + +Afterward, open http://localhost:8080 in your browser. + +That's it! diff --git a/advanced-integration/v2/server/php/composer.json b/advanced-integration/v2/server/php/composer.json new file mode 100644 index 00000000..4da469d0 --- /dev/null +++ b/advanced-integration/v2/server/php/composer.json @@ -0,0 +1,19 @@ +{ + "description": "PayPal JS SDK Advanced Integration - Checkout Flow", + "license": "MIT", + "require": { + "php": "^7.4 || ^8.0", + "ext-json": "*", + "paypal/paypal-server-sdk": "1.0.0" + }, + "require-dev": { + }, + "scripts": { + "start": [ + "Composer\\Config::disableProcessTimeout", + "php -S localhost:8080 -t src" + ] + }, + "config": { + } +} diff --git a/advanced-integration/v2/server/php/composer.lock b/advanced-integration/v2/server/php/composer.lock new file mode 100644 index 00000000..6b63abbe --- /dev/null +++ b/advanced-integration/v2/server/php/composer.lock @@ -0,0 +1,387 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e0fc9d83942f146aaa20785c7c614107", + "packages": [ + { + "name": "apimatic/core", + "version": "0.3.14", + "source": { + "type": "git", + "url": "https://github.com/apimatic/core-lib-php.git", + "reference": "c3eaad6cf0c00b793ce6d9bee8b87176247da582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/c3eaad6cf0c00b793ce6d9bee8b87176247da582", + "reference": "c3eaad6cf0c00b793ce6d9bee8b87176247da582", + "shasum": "" + }, + "require": { + "apimatic/core-interfaces": "~0.1.5", + "apimatic/jsonmapper": "^3.1.1", + "ext-curl": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "php": "^7.2 || ^8.0", + "php-jsonpointer/php-jsonpointer": "^3.0.2", + "psr/log": "^1.1.4 || ^2.0.0 || ^3.0.0" + }, + "require-dev": { + "phan/phan": "5.4.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Core logic and the utilities for the Apimatic's PHP SDK", + "homepage": "https://github.com/apimatic/core-lib-php", + "keywords": [ + "apimatic", + "core", + "corelib", + "php" + ], + "support": { + "issues": "https://github.com/apimatic/core-lib-php/issues", + "source": "https://github.com/apimatic/core-lib-php/tree/0.3.14" + }, + "time": "2025-02-27T06:03:30+00:00" + }, + { + "name": "apimatic/core-interfaces", + "version": "0.1.5", + "source": { + "type": "git", + "url": "https://github.com/apimatic/core-interfaces-php.git", + "reference": "b4f1bffc8be79584836f70af33c65e097eec155c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/core-interfaces-php/zipball/b4f1bffc8be79584836f70af33c65e097eec155c", + "reference": "b4f1bffc8be79584836f70af33c65e097eec155c", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "CoreInterfaces\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Definition of the behavior of apimatic/core, apimatic/unirest-php and Apimatic's PHP SDK", + "homepage": "https://github.com/apimatic/core-interfaces-php", + "keywords": [ + "apimatic", + "core", + "corelib", + "interface", + "php", + "unirest" + ], + "support": { + "issues": "https://github.com/apimatic/core-interfaces-php/issues", + "source": "https://github.com/apimatic/core-interfaces-php/tree/0.1.5" + }, + "time": "2024-05-09T06:32:07+00:00" + }, + { + "name": "apimatic/jsonmapper", + "version": "3.1.6", + "source": { + "type": "git", + "url": "https://github.com/apimatic/jsonmapper.git", + "reference": "c6cc21bd56bfe5d5822bbd08f514be465c0b24e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/jsonmapper/zipball/c6cc21bd56bfe5d5822bbd08f514be465c0b24e7", + "reference": "c6cc21bd56bfe5d5822bbd08f514be465c0b24e7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "squizlabs/php_codesniffer": "^3.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "apimatic\\jsonmapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "christian.weiske@netresearch.de", + "homepage": "http://www.netresearch.de/", + "role": "Developer" + }, + { + "name": "Mehdi Jaffery", + "email": "mehdi.jaffery@apimatic.io", + "homepage": "http://apimatic.io/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "mehdi.jaffery@apimatic.io", + "issues": "https://github.com/apimatic/jsonmapper/issues", + "source": "https://github.com/apimatic/jsonmapper/tree/3.1.6" + }, + "time": "2024-11-28T09:15:32+00:00" + }, + { + "name": "apimatic/unirest-php", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/apimatic/unirest-php.git", + "reference": "e16754010c16be5473289470f129d87a0f41b55e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/unirest-php/zipball/e16754010c16be5473289470f129d87a0f41b55e", + "reference": "e16754010c16be5473289470f129d87a0f41b55e", + "shasum": "" + }, + "require": { + "apimatic/core-interfaces": "^0.1.0", + "ext-curl": "*", + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phan/phan": "5.4.2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Unirest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mashape", + "email": "opensource@mashape.com", + "homepage": "https://www.mashape.com", + "role": "Developer" + }, + { + "name": "APIMATIC", + "email": "opensource@apimatic.io", + "homepage": "https://www.apimatic.io", + "role": "Developer" + } + ], + "description": "Unirest PHP", + "homepage": "https://github.com/apimatic/unirest-php", + "keywords": [ + "client", + "curl", + "http", + "https", + "rest" + ], + "support": { + "email": "opensource@apimatic.io", + "issues": "https://github.com/apimatic/unirest-php/issues", + "source": "https://github.com/apimatic/unirest-php/tree/4.0.5" + }, + "time": "2023-04-25T14:19:45+00:00" + }, + { + "name": "paypal/paypal-server-sdk", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/paypal/PayPal-PHP-Server-SDK.git", + "reference": "66fd3341bbffafe7d650fe7d9d2699fabf355e67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paypal/PayPal-PHP-Server-SDK/zipball/66fd3341bbffafe7d650fe7d9d2699fabf355e67", + "reference": "66fd3341bbffafe7d650fe7d9d2699fabf355e67", + "shasum": "" + }, + "require": { + "apimatic/core": "~0.3.13", + "apimatic/core-interfaces": "~0.1.5", + "apimatic/unirest-php": "^4.0.0", + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phan/phan": "5.4.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "PaypalServerSdkLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PayPal's SDK for interacting with the REST APIs", + "homepage": "https://github.com/paypal/PayPal-PHP-Server-SDK", + "support": { + "issues": "https://github.com/paypal/PayPal-PHP-Server-SDK/issues", + "source": "https://github.com/paypal/PayPal-PHP-Server-SDK/tree/1.0.0" + }, + "time": "2025-03-24T18:44:18+00:00" + }, + { + "name": "php-jsonpointer/php-jsonpointer", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/raphaelstolt/php-jsonpointer.git", + "reference": "4428f86c6f23846e9faa5a420c4ef14e485b3afb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/raphaelstolt/php-jsonpointer/zipball/4428f86c6f23846e9faa5a420c4ef14e485b3afb", + "reference": "4428f86c6f23846e9faa5a420c4ef14e485b3afb", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^1.11", + "phpunit/phpunit": "4.6.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Rs\\Json": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Stolt", + "email": "raphael.stolt@gmail.com", + "homepage": "http://raphaelstolt.blogspot.com/" + } + ], + "description": "Implementation of JSON Pointer (http://tools.ietf.org/html/rfc6901)", + "homepage": "https://github.com/raphaelstolt/php-jsonpointer", + "keywords": [ + "json", + "json pointer", + "json traversal" + ], + "support": { + "issues": "https://github.com/raphaelstolt/php-jsonpointer/issues", + "source": "https://github.com/raphaelstolt/php-jsonpointer/tree/master" + }, + "time": "2016-08-29T08:51:01+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4 || ^8.0", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/advanced-integration/v2/server/php/src/.htaccess b/advanced-integration/v2/server/php/src/.htaccess new file mode 100644 index 00000000..fe74ec5b --- /dev/null +++ b/advanced-integration/v2/server/php/src/.htaccess @@ -0,0 +1,34 @@ +Options All -Indexes + + +order allow,deny +deny from all + + + + RewriteEngine On + + # Redirect to HTTPS + # RewriteEngine On + # RewriteCond %{HTTPS} off + # RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + # Some hosts may require you to use the `RewriteBase` directive. + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + # If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the + # absolute physical path to the directory that contains this htaccess file. + # RewriteBase / + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + diff --git a/advanced-integration/v2/server/php/src/index.php b/advanced-integration/v2/server/php/src/index.php new file mode 100644 index 00000000..5fe563a8 --- /dev/null +++ b/advanced-integration/v2/server/php/src/index.php @@ -0,0 +1,117 @@ +clientCredentialsAuthCredentials( + ClientCredentialsAuthCredentialsBuilder::init( + $PAYPAL_CLIENT_ID, + $PAYPAL_CLIENT_SECRET + ) + ) + ->environment(Environment::SANDBOX) + ->build(); + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +function createOrder($cart) +{ + global $client; + + $orderBody = [ + 'body' => OrderRequestBuilder::init( + CheckoutPaymentIntent::CAPTURE, + [ + PurchaseUnitRequestBuilder::init( + AmountWithBreakdownBuilder::init( + 'USD', + '100.00' + )->build() + )->build() + ] + )->build() + ]; + + $apiResponse = $client->getOrdersController()->createOrder($orderBody); + + return handleResponse($apiResponse); +} + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +function captureOrder($orderID) +{ + global $client; + + $captureBody = [ + 'id' => $orderID + ]; + + $apiResponse = $client->getOrdersController()->captureOrder($captureBody); + + return handleResponse($apiResponse); +} + +function handleResponse($response) +{ + return [ + 'jsonResponse' => $response->getResult(), + 'httpStatusCode' => $response->getStatusCode() + ]; +} + +$endpoint = $_SERVER['REQUEST_URI']; +if ($endpoint === '/') { + try { + $response = [ + "message" => "Server is running" + ]; + header('Content-Type: application/json'); + echo json_encode($response); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} + +if ($endpoint === '/api/orders') { + $data = json_decode(file_get_contents('php://input'), true); + $cart = $data['cart']; + header('Content-Type: application/json'); + try { + $orderResponse = createOrder($cart); + echo json_encode($orderResponse['jsonResponse']); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} + + +if (str_ends_with($endpoint, '/capture')) { + $urlSegments = explode('/', $endpoint); + end($urlSegments); // Will set the pointer to the end of array + $orderID = prev($urlSegments); + header('Content-Type: application/json'); + try { + $captureResponse = captureOrder($orderID); + echo json_encode($captureResponse['jsonResponse']); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} diff --git a/advanced-integration/v2/server/python/.flaskenv b/advanced-integration/v2/server/python/.flaskenv new file mode 100644 index 00000000..c6cbe150 --- /dev/null +++ b/advanced-integration/v2/server/python/.flaskenv @@ -0,0 +1 @@ +FLASK_RUN_PORT=8080 \ No newline at end of file diff --git a/advanced-integration/v2/server/python/README.md b/advanced-integration/v2/server/python/README.md new file mode 100644 index 00000000..de523758 --- /dev/null +++ b/advanced-integration/v2/server/python/README.md @@ -0,0 +1,41 @@ +# Standard Integration Python Flask Sample + +PayPal Standard Integration sample in Python using Flask + +## Running the sample + +1. **Setup a virtual environment** + + ```sh + python3 -m venv .venv + ``` + +1. **Install the dependencies** + + ```sh + pip install -r requirements.txt + ``` + +1. **Add your API credentials to the environment:** + + - **Windows** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Unix** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +1. **Run the server** + + ```sh + flask --app server run + ``` + +1. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/advanced-integration/v2/server/python/requirements.txt b/advanced-integration/v2/server/python/requirements.txt new file mode 100644 index 00000000..361d7b0b --- /dev/null +++ b/advanced-integration/v2/server/python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +paypal-server-sdk==1.0.0 \ No newline at end of file diff --git a/advanced-integration/v2/server/python/server.py b/advanced-integration/v2/server/python/server.py new file mode 100644 index 00000000..09922c26 --- /dev/null +++ b/advanced-integration/v2/server/python/server.py @@ -0,0 +1,92 @@ +import logging +import os + +from flask import Flask, Response, request +from paypalserversdk.http.auth.o_auth_2 import ClientCredentialsAuthCredentials +from paypalserversdk.logging.configuration.api_logging_configuration import ( + LoggingConfiguration, + RequestLoggingConfiguration, + ResponseLoggingConfiguration, +) +from paypalserversdk.paypal_serversdk_client import PaypalServersdkClient +from paypalserversdk.controllers.orders_controller import OrdersController +from paypalserversdk.models.amount_with_breakdown import AmountWithBreakdown +from paypalserversdk.models.checkout_payment_intent import CheckoutPaymentIntent +from paypalserversdk.models.order_request import OrderRequest +from paypalserversdk.models.purchase_unit_request import PurchaseUnitRequest +from paypalserversdk.api_helper import ApiHelper + +app = Flask(__name__) + +paypal_client: PaypalServersdkClient = PaypalServersdkClient( + client_credentials_auth_credentials=ClientCredentialsAuthCredentials( + o_auth_client_id=os.getenv("PAYPAL_CLIENT_ID"), + o_auth_client_secret=os.getenv("PAYPAL_CLIENT_SECRET"), + ), + logging_configuration=LoggingConfiguration( + log_level=logging.INFO, + # Disable masking of sensitive headers for Sandbox testing. + # This should be set to True (the default if unset)in production. + mask_sensitive_headers=False, + request_logging_config=RequestLoggingConfiguration( + log_headers=True, log_body=True + ), + response_logging_config=ResponseLoggingConfiguration( + log_headers=True, log_body=True + ), + ), +) + +""" +Health check +""" + + +@app.route("/", methods=["GET"]) +def index(): + return {"message": "Server is running"} + + +orders_controller: OrdersController = paypal_client.orders + +""" +Create an order to start the transaction. + +@see https://developer.paypal.com/docs/api/orders/v2/#orders_create +""" +@app.route("/api/orders", methods=["POST"]) +def create_order(): + request_body = request.get_json() + # use the cart information passed from the front-end to calculate the order amount detals + cart = request_body["cart"] + order = orders_controller.create_order( + { + "body": OrderRequest( + intent=CheckoutPaymentIntent.CAPTURE, + purchase_units=[ + PurchaseUnitRequest( + AmountWithBreakdown(currency_code="USD", value="100.00") + ) + ], + ), + "prefer": "return=representation", + } + ) + return Response( + ApiHelper.json_serialize(order.body), status=200, mimetype="application/json" + ) + + +""" + Capture payment for the created order to complete the transaction. + + @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture +""" +@app.route("/api/orders//capture", methods=["POST"]) +def capture_order(order_id): + order = orders_controller.capture_order( + {"id": order_id, "prefer": "return=representation"} + ) + return Response( + ApiHelper.json_serialize(order.body), status=200, mimetype="application/json" + ) diff --git a/advanced-integration/v2/server/ruby/.ruby-version b/advanced-integration/v2/server/ruby/.ruby-version new file mode 100644 index 00000000..fa7adc7a --- /dev/null +++ b/advanced-integration/v2/server/ruby/.ruby-version @@ -0,0 +1 @@ +3.3.5 diff --git a/advanced-integration/v2/server/ruby/Gemfile b/advanced-integration/v2/server/ruby/Gemfile new file mode 100644 index 00000000..8772983f --- /dev/null +++ b/advanced-integration/v2/server/ruby/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "paypal-server-sdk", "~> 1.0.0" +gem "puma", "~> 6.4" +gem "rackup", "~> 2.1" +gem "sinatra", "~> 4.0" +gem "sinatra-contrib", "~> 4.0" \ No newline at end of file diff --git a/advanced-integration/v2/server/ruby/Gemfile.lock b/advanced-integration/v2/server/ruby/Gemfile.lock new file mode 100644 index 00000000..64193644 --- /dev/null +++ b/advanced-integration/v2/server/ruby/Gemfile.lock @@ -0,0 +1,123 @@ +GEM + remote: https://rubygems.org/ + specs: + apimatic_core (0.3.13) + apimatic_core_interfaces (~> 0.2.0) + certifi (~> 2018.1, >= 2018.01.18) + faraday-multipart (~> 1.0) + nokogiri (~> 1.13, >= 1.13.10) + apimatic_core_interfaces (0.2.1) + apimatic_faraday_client_adapter (0.1.4) + apimatic_core_interfaces (~> 0.2.0) + certifi (~> 2018.1, >= 2018.01.18) + faraday (~> 2.0, >= 2.0.1) + faraday-follow_redirects (~> 0.2) + faraday-gzip (~> 1.0) + faraday-http-cache (~> 2.2) + faraday-multipart (~> 1.0) + faraday-net_http_persistent (~> 2.0) + faraday-retry (~> 2.0) + base64 (0.2.0) + certifi (2018.01.18) + connection_pool (2.5.0) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-gzip (1.0.0) + faraday (>= 1.0) + zlib (~> 2.1) + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + faraday-net_http_persistent (2.3.0) + faraday (~> 2.5) + net-http-persistent (>= 4.0.4, < 5) + faraday-retry (2.2.1) + faraday (~> 2.0) + json (2.10.2) + logger (1.6.6) + multi_json (1.15.0) + multipart-post (2.4.1) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + net-http (0.6.0) + uri + net-http-persistent (4.0.5) + connection_pool (~> 2.2) + nio4r (2.7.4) + nokogiri (1.18.5-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.5-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.5-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-linux-musl) + racc (~> 1.4) + paypal-server-sdk (1.0.0) + apimatic_core (~> 0.3.11) + apimatic_core_interfaces (~> 0.2.1) + apimatic_faraday_client_adapter (~> 0.1.4) + puma (6.6.0) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.1.12) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.2.1) + rack (>= 3) + ruby2_keywords (0.0.5) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + sinatra-contrib (4.1.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.1.1) + sinatra (= 4.1.1) + tilt (~> 2.0) + tilt (2.6.0) + uri (1.0.3) + zlib (2.1.1) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + paypal-server-sdk (~> 1.0.0) + puma (~> 6.4) + rackup (~> 2.1) + sinatra (~> 4.0) + sinatra-contrib (~> 4.0) + +BUNDLED WITH + 2.5.18 diff --git a/advanced-integration/v2/server/ruby/README.md b/advanced-integration/v2/server/ruby/README.md new file mode 100644 index 00000000..6ad18fe2 --- /dev/null +++ b/advanced-integration/v2/server/ruby/README.md @@ -0,0 +1,37 @@ +# Standard Integration Ruby Sinatra Sample + +PayPal Standard Integration sample in Ruby using Sinatra + +## Running the sample + +1. **Ensure you have a supported Ruby version installed**: [Ruby Maintenance Branches](https://www.ruby-lang.org/en/downloads/branches/) + +1. **Install the dependencies** + + ```bash + bundle install + ``` + +1. **Add your API credentials to the environment:** + + - **Windows** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Unix** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +1. **Run the server** + + ```bash + bundle exec ruby server.rb + ``` + +1. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/advanced-integration/v2/server/ruby/server.rb b/advanced-integration/v2/server/ruby/server.rb new file mode 100644 index 00000000..b87bace7 --- /dev/null +++ b/advanced-integration/v2/server/ruby/server.rb @@ -0,0 +1,67 @@ +require 'paypal_server_sdk' +require 'sinatra' +require 'sinatra/json' + +include PaypalServerSdk + +set :port, 8080 + +paypal_client = PaypalServerSdk::Client.new( + client_credentials_auth_credentials: ClientCredentialsAuthCredentials.new( + o_auth_client_id: ENV['PAYPAL_CLIENT_ID'], + o_auth_client_secret: ENV['PAYPAL_CLIENT_SECRET'] + ), + environment: Environment::SANDBOX, + logging_configuration: LoggingConfiguration.new( + mask_sensitive_headers: false, + log_level: Logger::INFO, + request_logging_config: RequestLoggingConfiguration.new( + log_headers: true, + log_body: true, + ), + response_logging_config: ResponseLoggingConfiguration.new( + log_headers: true, + log_body: true + ) + ) +) + +# Health Check +get '/' do + json :message => "Server is running" +end + +# Create an order to start the transaction. +# +# @see https://developer.paypal.com/docs/api/orders/v2/#orders_create +post "/api/orders" do + # use the cart information passed from the front-end to calculate the order amount detals + cart = JSON.parse request.body.read + order_response = paypal_client.orders.create_order({ + 'body' => OrderRequest.new( + intent: CheckoutPaymentIntent::CAPTURE, + purchase_units: [ + PurchaseUnitRequest.new( + amount: AmountWithBreakdown.new( + currency_code: 'USD', + value: '100.00' + ) + ) + ] + ), + 'prefer' => 'return=representation' + }) + json order_response.data +end + +# Capture payment for the created order to complete the transaction. +# +# @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture +post '/api/orders/:order_id/capture' do |order_id| + capture_response = paypal_client.orders.capture_order({ + 'id' => order_id, + 'prefer' => 'return=representation' + }) + json capture_response.data +rescue ErrorException => e +end \ No newline at end of file diff --git a/save-payment-method/.env.example b/save-payment-method/.env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/save-payment-method/.env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE diff --git a/save-payment-method/.gitignore b/save-payment-method/.gitignore new file mode 100644 index 00000000..4c49bd78 --- /dev/null +++ b/save-payment-method/.gitignore @@ -0,0 +1 @@ +.env diff --git a/save-payment-method/README.md b/save-payment-method/README.md new file mode 100644 index 00000000..f1f4a0e9 --- /dev/null +++ b/save-payment-method/README.md @@ -0,0 +1,15 @@ +# Save Payment Method Example + +This folder contains example code for a PayPal Save Payment Method integration using both the JS SDK and Node.js to complete transactions with the PayPal REST API. + +[View the Documentation](https://developer.paypal.com/docs/checkout/save-payment-methods/during-purchase/js-sdk/paypal/) + +## Instructions + +1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create) +2. Rename `.env.example` to `.env` and update `PAYPAL_CLIENT_ID` and `PAYPAL_CLIENT_SECRET` +3. Replace `test` in [client/app.js](client/app.js) with your app's client-id +4. Run `npm install` +5. Run `npm start` +6. Open http://localhost:8888 +7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts) diff --git a/save-payment-method/client/app.js b/save-payment-method/client/app.js new file mode 100644 index 00000000..eebd2017 --- /dev/null +++ b/save-payment-method/client/app.js @@ -0,0 +1,97 @@ +window.paypal + .Buttons({ + async createOrder() { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + console.error(error); + resultMessage(`Could not initiate PayPal Checkout...

${error}`); + } + }, + async onApprove(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0]; + + if (errorDetail?.issue === "INSTRUMENT_DECLINED") { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error(`${errorDetail.description} (${orderData.debug_id})`); + } else if (!orderData.purchase_units) { + throw new Error(JSON.stringify(orderData)); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + resultMessage( + `Transaction ${transaction.status}: ${transaction.id}

See console for all available details.
+ See the return buyer experience + `, + ); + + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2), + ); + } + } catch (error) { + console.error(error); + resultMessage( + `Sorry, your transaction could not be processed...

${error}`, + ); + } + }, + }) + .render("#paypal-button-container"); + +// Example function to show a result to the user. Your site's UI library can be used instead. +function resultMessage(message) { + const container = document.querySelector("#result-message"); + container.innerHTML = message; +} diff --git a/save-payment-method/package.json b/save-payment-method/package.json new file mode 100644 index 00000000..d1596b09 --- /dev/null +++ b/save-payment-method/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-save-payment-method", + "description": "Sample Node.js web app to integrate PayPal Save Payment Method for online payments", + "version": "1.0.0", + "main": "server/server.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "nodemon server/server.js", + "format": "npx prettier --write **/*.{js,md}", + "format:check": "npx prettier --check **/*.{js,md}", + "lint": "npx eslint server/*.js client/*.js --no-config-lookup" + }, + "license": "Apache-2.0", + "dependencies": { + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/save-payment-method/server/server.js b/save-payment-method/server/server.js new file mode 100644 index 00000000..7d2596f2 --- /dev/null +++ b/save-payment-method/server/server.js @@ -0,0 +1,193 @@ +import express from "express"; +import fetch from "node-fetch"; +import "dotenv/config"; + +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8888 } = process.env; +const base = "https://api-m.sandbox.paypal.com"; +const app = express(); + +app.set("view engine", "ejs"); +app.set("views", "./server/views"); + +// host static files +app.use(express.static("client")); + +// parse post params sent in body in json format +app.use(express.json()); + +/** + * Generate an OAuth 2.0 access token for authenticating with PayPal REST APIs. + * @see https://developer.paypal.com/api/rest/authentication/ + */ +const authenticate = async (bodyParams) => { + const params = { + grant_type: "client_credentials", + response_type: "id_token", + ...bodyParams, + }; + + // pass the url encoded value as the body of the post call + const urlEncodedParams = new URLSearchParams(params).toString(); + try { + if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { + throw new Error("MISSING_API_CREDENTIALS"); + } + const auth = Buffer.from( + PAYPAL_CLIENT_ID + ":" + PAYPAL_CLIENT_SECRET, + ).toString("base64"); + + const response = await fetch(`${base}/v1/oauth2/token`, { + method: "POST", + body: urlEncodedParams, + headers: { + Authorization: `Basic ${auth}`, + }, + }); + return handleResponse(response); + } catch (error) { + console.error("Failed to generate Access Token:", error); + } +}; + +const generateAccessToken = async () => { + const { jsonResponse } = await authenticate(); + return jsonResponse.access_token; +}; + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +const createOrder = async (cart) => { + // use the cart information passed from the front-end to calculate the purchase unit details + console.log( + "shopping cart information passed from the frontend createOrder() callback:", + cart, + ); + + const accessToken = await generateAccessToken(); + const url = `${base}/v2/checkout/orders`; + const payload = { + intent: "CAPTURE", + purchase_units: [ + { + amount: { + currency_code: "USD", + value: "110.00", + }, + }, + ], + payment_source: { + paypal: { + attributes: { + vault: { + store_in_vault: "ON_SUCCESS", + usage_type: "MERCHANT", + customer_type: "CONSUMER", + }, + }, + experience_context: { + return_url: "http://example.com", + cancel_url: "http://example.com", + shipping_preference: "NO_SHIPPING", + }, + }, + }, + }; + + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation: + // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // "PayPal-Mock-Response": '{"mock_application_codes": "MISSING_REQUIRED_PARAMETER"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "PERMISSION_DENIED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' + }, + method: "POST", + body: JSON.stringify(payload), + }); + + return handleResponse(response); +}; + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +const captureOrder = async (orderID) => { + const accessToken = await generateAccessToken(); + const url = `${base}/v2/checkout/orders/${orderID}/capture`; + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + // Uncomment one of these to force an error for negative testing (in sandbox mode only). Documentation: + // https://developer.paypal.com/tools/sandbox/negative-testing/request-headers/ + // "PayPal-Mock-Response": '{"mock_application_codes": "INSTRUMENT_DECLINED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "TRANSACTION_REFUSED"}' + // "PayPal-Mock-Response": '{"mock_application_codes": "INTERNAL_SERVER_ERROR"}' + }, + }); + + return handleResponse(response); +}; + +async function handleResponse(response) { + try { + const jsonResponse = await response.json(); + return { + jsonResponse, + httpStatusCode: response.status, + }; + } catch (err) { + const errorMessage = await response.text(); + throw new Error(errorMessage); + } +} + +app.post("/api/orders", async (req, res) => { + try { + // use the cart information passed from the front-end to calculate the order amount detals + const { cart } = req.body; + const { jsonResponse, httpStatusCode } = await createOrder(cart); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to create order." }); + } +}); + +app.post("/api/orders/:orderID/capture", async (req, res) => { + try { + const { orderID } = req.params; + const { jsonResponse, httpStatusCode } = await captureOrder(orderID); + console.log("capture response", jsonResponse); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); + } +}); + +// render checkout page with client id & user id token +app.get("/", async (req, res) => { + try { + const { jsonResponse } = await authenticate({ + target_customer_id: req.query.customerID, + }); + res.render("checkout", { + clientId: PAYPAL_CLIENT_ID, + userIdToken: jsonResponse.id_token, + }); + } catch (err) { + res.status(500).send(err.message); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/save-payment-method/server/views/checkout.ejs b/save-payment-method/server/views/checkout.ejs new file mode 100644 index 00000000..69cd3e78 --- /dev/null +++ b/save-payment-method/server/views/checkout.ejs @@ -0,0 +1,17 @@ + + + + + + PayPal JS SDK Save Payment Method Integration + + +
+

+ + + + diff --git a/standard-integration/.env b/standard-integration/.env deleted file mode 100644 index f72af4c7..00000000 --- a/standard-integration/.env +++ /dev/null @@ -1,2 +0,0 @@ -CLIENT_ID= -APP_SECRET= \ No newline at end of file diff --git a/standard-integration/README.md b/standard-integration/README.md deleted file mode 100644 index 7220aea8..00000000 --- a/standard-integration/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Standard Integration Example - -This folder contains example code for a standard PayPal integration using both the JS SDK and node.js to complete transactions with the PayPal REST API. - -## Instructions - -1. [Create an application](https://developer.paypal.com/dashboard/applications/sandbox/create) -3. Add your app's `CLIENT_ID` and `APP_SECRET` to the `.env` file -2. Replace `test` in `public/index.html` with your app's client-id -4. Run `npm install` -5. Run `npm start` -6. Open http://localhost:8888 -7. Click "PayPal" and log in with one of your [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts). diff --git a/standard-integration/client/html/.env.example b/standard-integration/client/html/.env.example new file mode 100644 index 00000000..dbfed9bd --- /dev/null +++ b/standard-integration/client/html/.env.example @@ -0,0 +1,4 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE diff --git a/standard-integration/client/html/.gitignore b/standard-integration/client/html/.gitignore new file mode 100644 index 00000000..ed648a05 --- /dev/null +++ b/standard-integration/client/html/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.local \ No newline at end of file diff --git a/standard-integration/client/html/README.md b/standard-integration/client/html/README.md new file mode 100644 index 00000000..fba52a4b --- /dev/null +++ b/standard-integration/client/html/README.md @@ -0,0 +1,74 @@ +# Standard Integration with PayPal : HTML/JS + +## Getting Started + +This guide will walk you through setting up and running the HTML/JS Standard Integration locally. + +### Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Installation + +```bash +npm install +``` + +### Configuration + +1. Environmental Variables (.env) + + - Rename the .env.example file to .env + - Update the following keys with their actual values - + + ```bash + PAYPAL_CLIENT_ID= + ``` + +2. Connecting the client and server (vite.config.js) + + - Open vite.config.js in the root directory. + - Locate the proxy configuration object. + - Update the proxy key to match the server's address and port. For example: + + ```js + export default defineConfig({ + + server: { + proxy: { + "/api": { + target: "http://localhost:8080", // Replace with your server URL + changeOrigin: true, + }, + }, + }, + }); + ``` + +3. Starting the development server + + - **Start the server**: Follow the instructions in the server's README to start it. Typically, this involves running npm run start or a similar command in the server directory. + + - **Start the client**: + + ```bash + npm run start + ``` + + This will start the development server, and you should be able to access the Standard Checkout Page in your browser at `http://localhost:3000` (or the port specfied in the terminal output). + +### Additional Notes + +- **Server Setup**: Make sure you have the server up and running before starting the client. +- **Environment Variables**: Carefully configure the environment variables in the .env file to match your setup. +- **Proxy Configuration**: The proxy setting in vite.config.js is crucial for routing API requests from the client to the server during development. diff --git a/standard-integration/client/html/package.json b/standard-integration/client/html/package.json new file mode 100644 index 00000000..4685f847 --- /dev/null +++ b/standard-integration/client/html/package.json @@ -0,0 +1,17 @@ +{ + "name": "paypal-standard-integration-frontend-html", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "preview": "vite preview", + "start": "vite", + "format": "npx prettier --write **/*.{js,md}", + "format:check": "npx prettier --check **/*.{js,md}" + }, + "devDependencies": { + "dotenv": "^16.4.5", + "vite": "^5.4.2" + } +} diff --git a/standard-integration/client/html/src/app.js b/standard-integration/client/html/src/app.js new file mode 100644 index 00000000..685b2c46 --- /dev/null +++ b/standard-integration/client/html/src/app.js @@ -0,0 +1,106 @@ +window.paypal + .Buttons({ + style: { + shape: "rect", + layout: "vertical", + color: "gold", + label: "paypal", + }, + message: { + amount: 100, + }, + + async createOrder() { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } catch (error) { + console.error(error); + resultMessage(`Could not initiate PayPal Checkout...

${error}`); + } + }, + + async onApprove(data, actions) { + try { + const response = await fetch(`/api/orders/${data.orderID}/capture`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0]; + + if (errorDetail?.issue === "INSTRUMENT_DECLINED") { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per + // https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error(`${errorDetail.description} (${orderData.debug_id})`); + } else if (!orderData.purchase_units) { + throw new Error(JSON.stringify(orderData)); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + const transaction = + orderData?.purchase_units?.[0]?.payments?.captures?.[0] || + orderData?.purchase_units?.[0]?.payments?.authorizations?.[0]; + resultMessage( + `Transaction ${transaction.status}: ${transaction.id}
+
See console for all available details` + ); + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2) + ); + } + } catch (error) { + console.error(error); + resultMessage( + `Sorry, your transaction could not be processed...

${error}` + ); + } + }, + }) + .render("#paypal-button-container"); + +// Example function to show a result to the user. Your site's UI library can be used instead. +function resultMessage(message) { + const container = document.querySelector("#result-message"); + container.innerHTML = message; +} diff --git a/standard-integration/client/html/src/index.html b/standard-integration/client/html/src/index.html new file mode 100644 index 00000000..f993cfc1 --- /dev/null +++ b/standard-integration/client/html/src/index.html @@ -0,0 +1,20 @@ + + + + + + PayPal JS SDK Standard Integration + + + +
+

+ + + + + diff --git a/standard-integration/client/html/vite.config.js b/standard-integration/client/html/vite.config.js new file mode 100644 index 00000000..0a14da0e --- /dev/null +++ b/standard-integration/client/html/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [], + envDir: "../", + envPrefix: "PAYPAL", + root: "src", + server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + }, + }, +}) \ No newline at end of file diff --git a/standard-integration/client/react/.env.example b/standard-integration/client/react/.env.example new file mode 100644 index 00000000..abea0386 --- /dev/null +++ b/standard-integration/client/react/.env.example @@ -0,0 +1 @@ +PAYPAL_CLIENT_ID=PAYPAL_CLIENT_ID \ No newline at end of file diff --git a/standard-integration/client/react/.gitignore b/standard-integration/client/react/.gitignore new file mode 100644 index 00000000..ed648a05 --- /dev/null +++ b/standard-integration/client/react/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.local \ No newline at end of file diff --git a/standard-integration/client/react/README.md b/standard-integration/client/react/README.md new file mode 100644 index 00000000..208ccd52 --- /dev/null +++ b/standard-integration/client/react/README.md @@ -0,0 +1,74 @@ +# Standard Integration with PayPal : React + +## Getting Started + +This guide will walk you through setting up and running the React Standard Integration locally. + +### Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +### Installation + +```bash +npm install +``` + +### Configuration + +1. Environmental Variables (.env) + + - Rename the .env.example file to .env + - Update the following keys with their actual values - + + ```bash + PAYPAL_CLIENT_ID= + ``` + +2. Connecting the client and server (vite.config.js) + + - Open vite.config.js in the root directory. + - Locate the proxy configuration object. + - Update the proxy key to match the server's address and port. For example: + + ```js + export default defineConfig({ + + server: { + proxy: { + "/api": { + target: "http://localhost:8080", // Replace with your server URL + changeOrigin: true, + }, + }, + }, + }); + ``` + +3. Starting the development server + + - **Start the server**: Follow the instructions in the server's README to start it. Typically, this involves running npm run start or a similar command in the server directory. + + - **Start the client**: + + ```bash + npm run start + ``` + + This will start the development server, and you should be able to access the Standard Checkout Page in your browser at `http://localhost:3000` (or the port specfied in the terminal output). + +### Additional Notes + +- **Server Setup**: Make sure you have the server up and running before starting the client. +- **Environment Variables**: Carefully configure the environment variables in the .env file to match your setup. +- **Proxy Configuration**: The proxy setting in vite.config.js is crucial for routing API requests from the client to the server during development. diff --git a/standard-integration/client/react/package.json b/standard-integration/client/react/package.json new file mode 100644 index 00000000..2ce526a8 --- /dev/null +++ b/standard-integration/client/react/package.json @@ -0,0 +1,24 @@ +{ + "name": "paypal-standard-integration-frontend-react", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@paypal/react-paypal-js": "^8.6.0", + "dotenv": "^16.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "scripts": { + "client-dev": "vite", + "client-build": "vite build", + "client-preview": "vite preview", + "start": "npm run client-dev", + "format": "npx prettier --write **/*.{js,jsx,md}", + "format:check": "npx prettier --check **/*.{js,jsx,md}" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "vite": "^5.4.2" + } +} diff --git a/standard-integration/client/react/src/App.jsx b/standard-integration/client/react/src/App.jsx new file mode 100644 index 00000000..ec740440 --- /dev/null +++ b/standard-integration/client/react/src/App.jsx @@ -0,0 +1,123 @@ +import React, { useState } from "react"; +import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js"; + +// Renders errors or successfull transactions on the screen. +function Message({ content }) { + return

{content}

; +} + +function App() { + const initialOptions = { + "client-id": import.meta.env.PAYPAL_CLIENT_ID, + "enable-funding": "venmo", + "buyer-country": "US", + currency: "USD", + components: "buttons", + }; + + const [message, setMessage] = useState(""); + + return ( +
+ + { + try { + const response = await fetch("/api/orders", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + // use the "body" param to optionally pass additional order information + // like product ids and quantities + body: JSON.stringify({ + cart: [ + { + id: "YOUR_PRODUCT_ID", + quantity: "YOUR_PRODUCT_QUANTITY", + }, + ], + }), + }); + + const orderData = await response.json(); + + if (orderData.id) { + return orderData.id; + } else { + const errorDetail = orderData?.details?.[0]; + const errorMessage = errorDetail + ? `${errorDetail.issue} ${errorDetail.description} (${orderData.debug_id})` + : JSON.stringify(orderData); + + throw new Error(errorMessage); + } + } catch (error) { + console.error(error); + setMessage(`Could not initiate PayPal Checkout...${error}`); + } + }} + onApprove={async (data, actions) => { + try { + const response = await fetch( + `/api/orders/${data.orderID}/capture`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + } + ); + + const orderData = await response.json(); + // Three cases to handle: + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // (2) Other non-recoverable errors -> Show a failure message + // (3) Successful transaction -> Show confirmation or thank you message + + const errorDetail = orderData?.details?.[0]; + + if (errorDetail?.issue === "INSTRUMENT_DECLINED") { + // (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart() + // recoverable state, per https://developer.paypal.com/docs/checkout/standard/customize/handle-funding-failures/ + return actions.restart(); + } else if (errorDetail) { + // (2) Other non-recoverable errors -> Show a failure message + throw new Error( + `${errorDetail.description} (${orderData.debug_id})` + ); + } else { + // (3) Successful transaction -> Show confirmation or thank you message + // Or go to another URL: actions.redirect('thank_you.html'); + const transaction = + orderData.purchase_units[0].payments.captures[0]; + setMessage( + `Transaction ${transaction.status}: ${transaction.id}. See console for all available details` + ); + console.log( + "Capture result", + orderData, + JSON.stringify(orderData, null, 2) + ); + } + } catch (error) { + console.error(error); + setMessage( + `Sorry, your transaction could not be processed...${error}` + ); + } + }} + /> + + +
+ ); +} + +export default App; diff --git a/standard-integration/client/react/src/index.html b/standard-integration/client/react/src/index.html new file mode 100644 index 00000000..1de16dbb --- /dev/null +++ b/standard-integration/client/react/src/index.html @@ -0,0 +1,17 @@ + + + + + + React PayPal JS SDK Standard Integration + + + +
+ + + diff --git a/standard-integration/client/react/src/main.jsx b/standard-integration/client/react/src/main.jsx new file mode 100644 index 00000000..569fdf2f --- /dev/null +++ b/standard-integration/client/react/src/main.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App.jsx"; + +ReactDOM.createRoot(document.getElementById("root")).render( + + + +); diff --git a/standard-integration/client/react/vite.config.js b/standard-integration/client/react/vite.config.js new file mode 100644 index 00000000..274f8cf5 --- /dev/null +++ b/standard-integration/client/react/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + root: "src", + envDir: "../", + envPrefix: "PAYPAL", + server: { + port: 3000, + proxy: { + "/api": { + target: "http://localhost:8080", + changeOrigin: true, + secure: false, + }, + }, + }, +}) \ No newline at end of file diff --git a/standard-integration/package.json b/standard-integration/package.json deleted file mode 100644 index 766860ec..00000000 --- a/standard-integration/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@paypalcorp/standard-integration", - "version": "1.0.0", - "main": "paypal-api.js", - "type": "module", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node server.js" - }, - "author": "", - "license": "Apache-2.0", - "description": "", - "dependencies": { - "dotenv": "^16.0.0", - "express": "^4.17.3", - "node-fetch": "^3.2.1" - } -} diff --git a/standard-integration/paypal-api.js b/standard-integration/paypal-api.js deleted file mode 100644 index 35cca914..00000000 --- a/standard-integration/paypal-api.js +++ /dev/null @@ -1,66 +0,0 @@ -import fetch from "node-fetch"; - -const { CLIENT_ID, APP_SECRET } = process.env; -const base = "https://api-m.sandbox.paypal.com"; - -export async function createOrder() { - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - intent: "CAPTURE", - purchase_units: [ - { - amount: { - currency_code: "USD", - value: "100.00", - }, - }, - ], - }), - }); - - return handleResponse(response); -} - -export async function capturePayment(orderId) { - const accessToken = await generateAccessToken(); - const url = `${base}/v2/checkout/orders/${orderId}/capture`; - const response = await fetch(url, { - method: "post", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - }); - - return handleResponse(response); -} - -export async function generateAccessToken() { - const auth = Buffer.from(CLIENT_ID + ":" + APP_SECRET).toString("base64"); - const response = await fetch(`${base}/v1/oauth2/token`, { - method: "post", - body: "grant_type=client_credentials", - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - const jsonData = await handleResponse(response); - return jsonData.access_token; -} - -async function handleResponse(response) { - if (response.status === 200 || response.status === 201) { - return response.json(); - } - - const errorMessage = await response.text(); - throw new Error(errorMessage); -} diff --git a/standard-integration/public/index.html b/standard-integration/public/index.html deleted file mode 100644 index f0f8b659..00000000 --- a/standard-integration/public/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - -
- - - diff --git a/standard-integration/server.js b/standard-integration/server.js deleted file mode 100644 index d9d40db4..00000000 --- a/standard-integration/server.js +++ /dev/null @@ -1,28 +0,0 @@ -import "dotenv/config"; // loads variables from .env file -import express from "express"; -import * as paypal from "./paypal-api.js"; - -const app = express(); - -app.use(express.static("public")); - -app.post("/api/orders", async (req, res) => { - try { - const order = await paypal.createOrder(); - res.json(order); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.post("/api/orders/:orderID/capture", async (req, res) => { - const { orderID } = req.params; - try { - const captureData = await paypal.capturePayment(orderID); - res.json(captureData); - } catch (err) { - res.status(500).send(err.message); - } -}); - -app.listen(8888); diff --git a/standard-integration/server/dotnet/.gitignore b/standard-integration/server/dotnet/.gitignore new file mode 100644 index 00000000..cbbd0b5c --- /dev/null +++ b/standard-integration/server/dotnet/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/standard-integration/server/dotnet/PayPalStandardIntegration.csproj b/standard-integration/server/dotnet/PayPalStandardIntegration.csproj new file mode 100644 index 00000000..f74550c1 --- /dev/null +++ b/standard-integration/server/dotnet/PayPalStandardIntegration.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + PayPalStandardIntegration + + + + + + + \ No newline at end of file diff --git a/standard-integration/server/dotnet/README.md b/standard-integration/server/dotnet/README.md new file mode 100644 index 00000000..2405572a --- /dev/null +++ b/standard-integration/server/dotnet/README.md @@ -0,0 +1,35 @@ +# Standard Integration .NET Sample + +PayPal Standard Integration sample in .NET + +## Running the sample + +1. **Add your API credentials to the environment:** + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. **Build the server** + + ```bash + dotnet restore + ``` + +3. **Run the server** + + ```bash + dotnet run + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/standard-integration/server/dotnet/Server.cs b/standard-integration/server/dotnet/Server.cs new file mode 100644 index 00000000..2b4a1c4f --- /dev/null +++ b/standard-integration/server/dotnet/Server.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using PaypalServerSdk.Standard; +using PaypalServerSdk.Standard.Authentication; +using PaypalServerSdk.Standard.Controllers; +using PaypalServerSdk.Standard.Http.Response; +using PaypalServerSdk.Standard.Models; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; + +namespace PayPalAdvancedIntegration; + +public class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseUrls("http://localhost:8080"); + webBuilder.UseStartup(); + }); +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().AddNewtonsoftJson(); + services.AddHttpClient(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + app.UseRouting(); + app.UseStaticFiles(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} + +[ApiController] +public class CheckoutController : Controller +{ + private readonly OrdersController _ordersController; + private readonly PaymentsController _paymentsController; + + private IConfiguration _configuration { get; } + private string _paypalClientId + { + get { return System.Environment.GetEnvironmentVariable("PAYPAL_CLIENT_ID"); } + } + private string _paypalClientSecret + { + get { return System.Environment.GetEnvironmentVariable("PAYPAL_CLIENT_SECRET"); } + } + + private readonly ILogger _logger; + + public CheckoutController(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + + // Initialize the PayPal SDK client + PaypalServerSdkClient client = new PaypalServerSdkClient.Builder() + .Environment(PaypalServerSdk.Standard.Environment.Sandbox) + .ClientCredentialsAuth( + new ClientCredentialsAuthModel.Builder(_paypalClientId, _paypalClientSecret).Build() + ) + .LoggingConfig(config => + config + .LogLevel(LogLevel.Information) + .RequestConfig(reqConfig => reqConfig.Body(true)) + .ResponseConfig(respConfig => respConfig.Headers(true)) + ) + .Build(); + + _ordersController = client.OrdersController; + _paymentsController = client.PaymentsController; + } + + [HttpPost("api/orders")] + public async Task CreateOrder([FromBody] dynamic cart) + { + try + { + var result = await _CreateOrder(cart); + return StatusCode((int)result.StatusCode, result.Data); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to create order:", ex); + return StatusCode(500, new { error = "Failed to create order." }); + } + } + + private async Task _CreateOrder(dynamic cart) + { + CheckoutPaymentIntent intent = (CheckoutPaymentIntent) + Enum.Parse(typeof(CheckoutPaymentIntent), "CAPTURE", true); + + CreateOrderInput createOrderInput = new CreateOrderInput + { + Body = new OrderRequest + { + Intent = intent, + PurchaseUnits = new List + { + new PurchaseUnitRequest + { + Amount = new AmountWithBreakdown { CurrencyCode = "USD", MValue = "100", }, + }, + }, + }, + }; + + ApiResponse result = await _ordersController.CreateOrderAsync(createOrderInput); + return result; + } + + [HttpPost("api/orders/{orderID}/capture")] + public async Task CaptureOrder(string orderID) + { + try + { + var result = await _CaptureOrder(orderID); + return StatusCode((int)result.StatusCode, result.Data); + } + catch (Exception ex) + { + Console.Error.WriteLine("Failed to capture order:", ex); + return StatusCode(500, new { error = "Failed to capture order." }); + } + } + + private async Task _CaptureOrder(string orderID) + { + CaptureOrderInput ordersCaptureInput = new CaptureOrderInput { Id = orderID, }; + + ApiResponse result = await _ordersController.CaptureOrderAsync(ordersCaptureInput); + + return result; + } +} diff --git a/standard-integration/server/java/.gitignore b/standard-integration/server/java/.gitignore new file mode 100644 index 00000000..9f970225 --- /dev/null +++ b/standard-integration/server/java/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/standard-integration/server/java/README.md b/standard-integration/server/java/README.md new file mode 100644 index 00000000..1a242c7a --- /dev/null +++ b/standard-integration/server/java/README.md @@ -0,0 +1,35 @@ +# Standard Integration Java Sample + +PayPal Standard Integration sample in Java + +## Running the sample + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Build the server + + ```bash + mvn clean install + ``` + +3. Run the server + + ```bash + mvn spring-boot:run + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/standard-integration/server/java/pom.xml b/standard-integration/server/java/pom.xml new file mode 100644 index 00000000..908af6b3 --- /dev/null +++ b/standard-integration/server/java/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.paypal.sample + advanced-integration + 0.0.1-SNAPSHOT + PayPal Standard Integration + Sample Java demo application for PayPal JS SDK Standard Integration + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + + com.paypal.sdk + + + paypal-server-sdk + + + 1.0.0 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/standard-integration/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java b/standard-integration/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java new file mode 100644 index 00000000..15f986c8 --- /dev/null +++ b/standard-integration/server/java/src/main/java/com/paypal/sample/SampleAppApplication.java @@ -0,0 +1,141 @@ +package com.paypal.sample; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.client.RestTemplate; + +import com.paypal.sdk.Environment; +import com.paypal.sdk.PaypalServerSdkClient; +import com.paypal.sdk.authentication.ClientCredentialsAuthModel; +import com.paypal.sdk.controllers.OrdersController; +import com.paypal.sdk.exceptions.ApiException; +import com.paypal.sdk.http.response.ApiResponse; +import com.paypal.sdk.models.AmountWithBreakdown; +import com.paypal.sdk.models.CheckoutPaymentIntent; +import com.paypal.sdk.models.Order; +import com.paypal.sdk.models.OrderRequest; +import com.paypal.sdk.models.CreateOrderInput; +import com.paypal.sdk.models.CaptureOrderInput; +import com.paypal.sdk.models.PurchaseUnitRequest; +import java.util.Arrays; +import org.slf4j.event.Level; + +import java.io.IOException; +import java.util.Map; + +@SpringBootApplication +public class SampleAppApplication { + + @Value("${PAYPAL_CLIENT_ID}") + private String PAYPAL_CLIENT_ID; + + @Value("${PAYPAL_CLIENT_SECRET}") + private String PAYPAL_CLIENT_SECRET; + + public static void main(String[] args) { + SpringApplication.run(SampleAppApplication.class, args); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public PaypalServerSdkClient paypalClient() { + return new PaypalServerSdkClient.Builder() + .loggingConfig(builder -> builder + .level(Level.DEBUG) + .requestConfig(logConfigBuilder -> logConfigBuilder.body(true)) + .responseConfig(logConfigBuilder -> logConfigBuilder.headers(true))) + .httpClientConfig(configBuilder -> configBuilder + .timeout(0)) + .environment(Environment.SANDBOX) + .clientCredentialsAuth(new ClientCredentialsAuthModel.Builder( + PAYPAL_CLIENT_ID, + PAYPAL_CLIENT_SECRET) + .build()) + .build(); + } + + + @Controller + @RequestMapping("/") + public class CheckoutController { + + private final ObjectMapper objectMapper; + private final PaypalServerSdkClient client; + + public CheckoutController(ObjectMapper objectMapper, PaypalServerSdkClient client) { + this.objectMapper = objectMapper; + this.client = client; + } + + @PostMapping("/api/orders") + public ResponseEntity createOrder(@RequestBody Map request) { + try { + String cart = objectMapper.writeValueAsString(request.get("cart")); + Order response = createOrder(cart); + return new ResponseEntity<>(response, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PostMapping("/api/orders/{orderID}/capture") + public ResponseEntity captureOrder(@PathVariable String orderID) { + try { + Order response = captureOrders(orderID); + return new ResponseEntity(response, HttpStatus.OK); + } catch (Exception e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private Order createOrder(String cart) throws IOException, ApiException { + + CreateOrderInput createOrderInput = new CreateOrderInput.Builder( + null, + new OrderRequest.Builder( + CheckoutPaymentIntent.CAPTURE, + Arrays.asList( + new PurchaseUnitRequest.Builder( + new AmountWithBreakdown.Builder( + "USD", + "100.00") + .build()) + .build())) + .build()) + .build(); + + OrdersController ordersController = client.getOrdersController(); + + ApiResponse apiResponse = ordersController.createOrder(createOrderInput); + + return apiResponse.getResult(); + } + + private Order captureOrders(String orderID) throws IOException, ApiException { + CaptureOrderInput ordersCaptureInput = new CaptureOrderInput.Builder( + orderID, + null) + .build(); + OrdersController ordersController = client.getOrdersController(); + ApiResponse apiResponse = ordersController.captureOrder(ordersCaptureInput); + return apiResponse.getResult(); + } + } +} diff --git a/standard-integration/server/java/src/main/resources/application.properties b/standard-integration/server/java/src/main/resources/application.properties new file mode 100644 index 00000000..103551b3 --- /dev/null +++ b/standard-integration/server/java/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=v2-java +PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID} +PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET} diff --git a/standard-integration/server/node/.env.example b/standard-integration/server/node/.env.example new file mode 100644 index 00000000..2251fbbb --- /dev/null +++ b/standard-integration/server/node/.env.example @@ -0,0 +1,5 @@ +# Create an application to obtain credentials at +# https://developer.paypal.com/dashboard/applications/sandbox + +PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_GOES_HERE +PAYPAL_CLIENT_SECRET=YOUR_SECRET_GOES_HERE diff --git a/standard-integration/server/node/.gitignore b/standard-integration/server/node/.gitignore new file mode 100644 index 00000000..21ee8d3d --- /dev/null +++ b/standard-integration/server/node/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.local \ No newline at end of file diff --git a/standard-integration/server/node/README.md b/standard-integration/server/node/README.md new file mode 100644 index 00000000..81657297 --- /dev/null +++ b/standard-integration/server/node/README.md @@ -0,0 +1,35 @@ +# Standard Integration Node.js Sample + +PayPal Standard Integration sample in Node.js + +## Running the sample + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Install the packages + + ```bash + npm install + ``` + +3. Run the server + + ```bash + npm run start + ``` + +4. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/standard-integration/server/node/package.json b/standard-integration/server/node/package.json new file mode 100644 index 00000000..ed8cf1e9 --- /dev/null +++ b/standard-integration/server/node/package.json @@ -0,0 +1,23 @@ +{ + "name": "paypal-standard-integration-backend-node", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@paypal/paypal-server-sdk": "^1.0.0", + "body-parser": "^1.20.3", + "dotenv": "^16.3.1", + "express": "^4.18.2" + }, + "scripts": { + "server-dev": "nodemon server.js", + "start": "npm run server-dev", + "prod": "node server.js", + "format": "npx prettier --write **/*.{js,jsx,md}", + "format:check": "npx prettier --check **/*.{js,jsx,md}" + }, + "devDependencies": { + "concurrently": "^8.2.1", + "nodemon": "^3.0.1" + } +} diff --git a/standard-integration/server/node/server.js b/standard-integration/server/node/server.js new file mode 100644 index 00000000..3633923a --- /dev/null +++ b/standard-integration/server/node/server.js @@ -0,0 +1,129 @@ +import express from "express"; +import "dotenv/config"; +import { + ApiError, + CheckoutPaymentIntent, + Client, + Environment, + LogLevel, + OrdersController, +} from "@paypal/paypal-server-sdk"; +import bodyParser from "body-parser"; + +const app = express(); +app.use(bodyParser.json()); + +const { PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PORT = 8080 } = process.env; + +const client = new Client({ + clientCredentialsAuthCredentials: { + oAuthClientId: PAYPAL_CLIENT_ID, + oAuthClientSecret: PAYPAL_CLIENT_SECRET, + }, + timeout: 0, + environment: Environment.Sandbox, + logging: { + logLevel: LogLevel.Info, + logRequest: { + logBody: true, + }, + logResponse: { + logHeaders: true, + }, + }, +}); + +const ordersController = new OrdersController(client); + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +const createOrder = async (cart) => { + const collect = { + body: { + intent: CheckoutPaymentIntent.Capture, + purchaseUnits: [ + { + amount: { + currencyCode: "USD", + value: "100.00", + }, + }, + ], + }, + prefer: "return=minimal", + }; + + try { + const { body, ...httpResponse } = await ordersController.createOrder( + collect + ); + // Get more response info... + // const { statusCode, headers } = httpResponse; + return { + jsonResponse: JSON.parse(body), + httpStatusCode: httpResponse.statusCode, + }; + } catch (error) { + if (error instanceof ApiError) { + // const { statusCode, headers } = error; + throw new Error(error.message); + } + } +}; + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +const captureOrder = async (orderID) => { + const collect = { + id: orderID, + prefer: "return=minimal", + }; + + try { + const { body, ...httpResponse } = await ordersController.captureOrder( + collect + ); + // Get more response info... + // const { statusCode, headers } = httpResponse; + return { + jsonResponse: JSON.parse(body), + httpStatusCode: httpResponse.statusCode, + }; + } catch (error) { + if (error instanceof ApiError) { + // const { statusCode, headers } = error; + throw new Error(error.message); + } + } +}; + +app.post("/api/orders", async (req, res) => { + try { + // use the cart information passed from the front-end to calculate the order amount detals + const { cart } = req.body; + const { jsonResponse, httpStatusCode } = await createOrder(cart); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to create order." }); + } +}); + +app.post("/api/orders/:orderID/capture", async (req, res) => { + try { + const { orderID } = req.params; + const { jsonResponse, httpStatusCode } = await captureOrder(orderID); + res.status(httpStatusCode).json(jsonResponse); + } catch (error) { + console.error("Failed to create order:", error); + res.status(500).json({ error: "Failed to capture order." }); + } +}); + +app.listen(PORT, () => { + console.log(`Node server listening at http://localhost:${PORT}/`); +}); diff --git a/standard-integration/server/php/.gitignore b/standard-integration/server/php/.gitignore new file mode 100644 index 00000000..a725465a --- /dev/null +++ b/standard-integration/server/php/.gitignore @@ -0,0 +1 @@ +vendor/ \ No newline at end of file diff --git a/standard-integration/server/php/README.md b/standard-integration/server/php/README.md new file mode 100644 index 00000000..dd42041d --- /dev/null +++ b/standard-integration/server/php/README.md @@ -0,0 +1,63 @@ +# Standard Integration Sample Application - PHP + +This sample app demonstrates how to integrate with ACDC using PayPal's REST APIs. + +## Before You Code + +1. **Setup a PayPal Account** + + To get started, you'll need a developer, personal, or business account. + + [Sign Up](https://www.paypal.com/signin/client?flow=provisionUser) or [Log In](https://www.paypal.com/signin?returnUri=https%253A%252F%252Fdeveloper.paypal.com%252Fdashboard&intent=developer) + + You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. + +2. **Create an Application** + + Once you've setup a PayPal account, you'll need to obtain a **Client ID** and **Secret**. [Create a sandbox application](https://developer.paypal.com/dashboard/applications/sandbox/create). + +## How to Run Locally + +1. Add your API credentials to the environment: + + - **Windows (powershell)** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Linux / MacOS** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +2. Follow the below instructions to setup & run server. + +## Install the Composer + +We'll be using Composer (https://getcomposer.org/) for dependency management. To install Composer on a Mac, run the following command in the terminal: + +```bash +brew install composer +``` + +Composer can be downloaded for Windows from this link: https://getcomposer.org/download/. + +## To install the dependencies + +```bash +composer install +``` + +## To run the application in development, you can run this command + +```bash +composer start +``` + +Afterward, open http://localhost:8080 in your browser. + +That's it! diff --git a/standard-integration/server/php/composer.json b/standard-integration/server/php/composer.json new file mode 100644 index 00000000..bffbaae1 --- /dev/null +++ b/standard-integration/server/php/composer.json @@ -0,0 +1,19 @@ +{ + "description": "PayPal JS SDK Standard Integration - Checkout Flow", + "license": "MIT", + "require": { + "php": "^7.4 || ^8.0", + "ext-json": "*", + "paypal/paypal-server-sdk": "1.0.0" + }, + "require-dev": { + }, + "scripts": { + "start": [ + "Composer\\Config::disableProcessTimeout", + "php -S localhost:8080 -t src" + ] + }, + "config": { + } +} \ No newline at end of file diff --git a/standard-integration/server/php/composer.lock b/standard-integration/server/php/composer.lock new file mode 100644 index 00000000..6b63abbe --- /dev/null +++ b/standard-integration/server/php/composer.lock @@ -0,0 +1,387 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e0fc9d83942f146aaa20785c7c614107", + "packages": [ + { + "name": "apimatic/core", + "version": "0.3.14", + "source": { + "type": "git", + "url": "https://github.com/apimatic/core-lib-php.git", + "reference": "c3eaad6cf0c00b793ce6d9bee8b87176247da582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/c3eaad6cf0c00b793ce6d9bee8b87176247da582", + "reference": "c3eaad6cf0c00b793ce6d9bee8b87176247da582", + "shasum": "" + }, + "require": { + "apimatic/core-interfaces": "~0.1.5", + "apimatic/jsonmapper": "^3.1.1", + "ext-curl": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "php": "^7.2 || ^8.0", + "php-jsonpointer/php-jsonpointer": "^3.0.2", + "psr/log": "^1.1.4 || ^2.0.0 || ^3.0.0" + }, + "require-dev": { + "phan/phan": "5.4.5", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Core logic and the utilities for the Apimatic's PHP SDK", + "homepage": "https://github.com/apimatic/core-lib-php", + "keywords": [ + "apimatic", + "core", + "corelib", + "php" + ], + "support": { + "issues": "https://github.com/apimatic/core-lib-php/issues", + "source": "https://github.com/apimatic/core-lib-php/tree/0.3.14" + }, + "time": "2025-02-27T06:03:30+00:00" + }, + { + "name": "apimatic/core-interfaces", + "version": "0.1.5", + "source": { + "type": "git", + "url": "https://github.com/apimatic/core-interfaces-php.git", + "reference": "b4f1bffc8be79584836f70af33c65e097eec155c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/core-interfaces-php/zipball/b4f1bffc8be79584836f70af33c65e097eec155c", + "reference": "b4f1bffc8be79584836f70af33c65e097eec155c", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "CoreInterfaces\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Definition of the behavior of apimatic/core, apimatic/unirest-php and Apimatic's PHP SDK", + "homepage": "https://github.com/apimatic/core-interfaces-php", + "keywords": [ + "apimatic", + "core", + "corelib", + "interface", + "php", + "unirest" + ], + "support": { + "issues": "https://github.com/apimatic/core-interfaces-php/issues", + "source": "https://github.com/apimatic/core-interfaces-php/tree/0.1.5" + }, + "time": "2024-05-09T06:32:07+00:00" + }, + { + "name": "apimatic/jsonmapper", + "version": "3.1.6", + "source": { + "type": "git", + "url": "https://github.com/apimatic/jsonmapper.git", + "reference": "c6cc21bd56bfe5d5822bbd08f514be465c0b24e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/jsonmapper/zipball/c6cc21bd56bfe5d5822bbd08f514be465c0b24e7", + "reference": "c6cc21bd56bfe5d5822bbd08f514be465c0b24e7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "squizlabs/php_codesniffer": "^3.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "apimatic\\jsonmapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "christian.weiske@netresearch.de", + "homepage": "http://www.netresearch.de/", + "role": "Developer" + }, + { + "name": "Mehdi Jaffery", + "email": "mehdi.jaffery@apimatic.io", + "homepage": "http://apimatic.io/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "mehdi.jaffery@apimatic.io", + "issues": "https://github.com/apimatic/jsonmapper/issues", + "source": "https://github.com/apimatic/jsonmapper/tree/3.1.6" + }, + "time": "2024-11-28T09:15:32+00:00" + }, + { + "name": "apimatic/unirest-php", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/apimatic/unirest-php.git", + "reference": "e16754010c16be5473289470f129d87a0f41b55e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/apimatic/unirest-php/zipball/e16754010c16be5473289470f129d87a0f41b55e", + "reference": "e16754010c16be5473289470f129d87a0f41b55e", + "shasum": "" + }, + "require": { + "apimatic/core-interfaces": "^0.1.0", + "ext-curl": "*", + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phan/phan": "5.4.2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Unirest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mashape", + "email": "opensource@mashape.com", + "homepage": "https://www.mashape.com", + "role": "Developer" + }, + { + "name": "APIMATIC", + "email": "opensource@apimatic.io", + "homepage": "https://www.apimatic.io", + "role": "Developer" + } + ], + "description": "Unirest PHP", + "homepage": "https://github.com/apimatic/unirest-php", + "keywords": [ + "client", + "curl", + "http", + "https", + "rest" + ], + "support": { + "email": "opensource@apimatic.io", + "issues": "https://github.com/apimatic/unirest-php/issues", + "source": "https://github.com/apimatic/unirest-php/tree/4.0.5" + }, + "time": "2023-04-25T14:19:45+00:00" + }, + { + "name": "paypal/paypal-server-sdk", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/paypal/PayPal-PHP-Server-SDK.git", + "reference": "66fd3341bbffafe7d650fe7d9d2699fabf355e67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paypal/PayPal-PHP-Server-SDK/zipball/66fd3341bbffafe7d650fe7d9d2699fabf355e67", + "reference": "66fd3341bbffafe7d650fe7d9d2699fabf355e67", + "shasum": "" + }, + "require": { + "apimatic/core": "~0.3.13", + "apimatic/core-interfaces": "~0.1.5", + "apimatic/unirest-php": "^4.0.0", + "ext-json": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phan/phan": "5.4.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "PaypalServerSdkLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PayPal's SDK for interacting with the REST APIs", + "homepage": "https://github.com/paypal/PayPal-PHP-Server-SDK", + "support": { + "issues": "https://github.com/paypal/PayPal-PHP-Server-SDK/issues", + "source": "https://github.com/paypal/PayPal-PHP-Server-SDK/tree/1.0.0" + }, + "time": "2025-03-24T18:44:18+00:00" + }, + { + "name": "php-jsonpointer/php-jsonpointer", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/raphaelstolt/php-jsonpointer.git", + "reference": "4428f86c6f23846e9faa5a420c4ef14e485b3afb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/raphaelstolt/php-jsonpointer/zipball/4428f86c6f23846e9faa5a420c4ef14e485b3afb", + "reference": "4428f86c6f23846e9faa5a420c4ef14e485b3afb", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^1.11", + "phpunit/phpunit": "4.6.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Rs\\Json": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Stolt", + "email": "raphael.stolt@gmail.com", + "homepage": "http://raphaelstolt.blogspot.com/" + } + ], + "description": "Implementation of JSON Pointer (http://tools.ietf.org/html/rfc6901)", + "homepage": "https://github.com/raphaelstolt/php-jsonpointer", + "keywords": [ + "json", + "json pointer", + "json traversal" + ], + "support": { + "issues": "https://github.com/raphaelstolt/php-jsonpointer/issues", + "source": "https://github.com/raphaelstolt/php-jsonpointer/tree/master" + }, + "time": "2016-08-29T08:51:01+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4 || ^8.0", + "ext-json": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/standard-integration/server/php/src/.htaccess b/standard-integration/server/php/src/.htaccess new file mode 100644 index 00000000..fe74ec5b --- /dev/null +++ b/standard-integration/server/php/src/.htaccess @@ -0,0 +1,34 @@ +Options All -Indexes + + +order allow,deny +deny from all + + + + RewriteEngine On + + # Redirect to HTTPS + # RewriteEngine On + # RewriteCond %{HTTPS} off + # RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + # Some hosts may require you to use the `RewriteBase` directive. + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ + RewriteRule ^(.*) - [E=BASE:%1] + + # If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the + # absolute physical path to the directory that contains this htaccess file. + # RewriteBase / + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + diff --git a/standard-integration/server/php/src/index.php b/standard-integration/server/php/src/index.php new file mode 100644 index 00000000..784baf4c --- /dev/null +++ b/standard-integration/server/php/src/index.php @@ -0,0 +1,118 @@ +clientCredentialsAuthCredentials( + ClientCredentialsAuthCredentialsBuilder::init( + $PAYPAL_CLIENT_ID, + $PAYPAL_CLIENT_SECRET + ) + ) + ->environment(Environment::SANDBOX) + ->build(); + +/** + * Create an order to start the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_create + */ +function createOrder($cart) +{ + global $client; + + $orderBody = [ + 'body' => OrderRequestBuilder::init( + CheckoutPaymentIntent::CAPTURE, + [ + PurchaseUnitRequestBuilder::init( + AmountWithBreakdownBuilder::init( + 'USD', + '100.00' + )->build() + )->build() + ] + )->build() + ]; + + $apiResponse = $client->getOrdersController()->createOrder($orderBody); + + return handleResponse($apiResponse); +} + +/** + * Capture payment for the created order to complete the transaction. + * @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture + */ +function captureOrder($orderID) +{ + global $client; + + $captureBody = [ + 'id' => $orderID + ]; + + $apiResponse = $client->getOrdersController()->captureOrder($captureBody); + + return handleResponse($apiResponse); +} + +function handleResponse($response) +{ + return [ + 'jsonResponse' => $response->getResult(), + 'httpStatusCode' => $response->getStatusCode() + ]; +} + +$endpoint = $_SERVER['REQUEST_URI']; +if ($endpoint === '/') { + try { + $response = [ + "message" => "Server is running" + ]; + header('Content-Type: application/json'); + echo json_encode($response); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} + +if ($endpoint === '/api/orders') { + $data = json_decode(file_get_contents('php://input'), true); + $cart = $data['cart']; + header('Content-Type: application/json'); + try { + $orderResponse = createOrder($cart); + echo json_encode($orderResponse['jsonResponse']); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} + + +if (str_ends_with($endpoint, '/capture')) { + $urlSegments = explode('/', $endpoint); + end($urlSegments); // Will set the pointer to the end of array + $orderID = prev($urlSegments); + header('Content-Type: application/json'); + try { + $captureResponse = captureOrder($orderID); + echo json_encode($captureResponse['jsonResponse']); + } catch (Exception $e) { + echo json_encode(['error' => $e->getMessage()]); + http_response_code(500); + } +} + diff --git a/standard-integration/server/python/.flaskenv b/standard-integration/server/python/.flaskenv new file mode 100644 index 00000000..c6cbe150 --- /dev/null +++ b/standard-integration/server/python/.flaskenv @@ -0,0 +1 @@ +FLASK_RUN_PORT=8080 \ No newline at end of file diff --git a/standard-integration/server/python/README.md b/standard-integration/server/python/README.md new file mode 100644 index 00000000..de523758 --- /dev/null +++ b/standard-integration/server/python/README.md @@ -0,0 +1,41 @@ +# Standard Integration Python Flask Sample + +PayPal Standard Integration sample in Python using Flask + +## Running the sample + +1. **Setup a virtual environment** + + ```sh + python3 -m venv .venv + ``` + +1. **Install the dependencies** + + ```sh + pip install -r requirements.txt + ``` + +1. **Add your API credentials to the environment:** + + - **Windows** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Unix** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +1. **Run the server** + + ```sh + flask --app server run + ``` + +1. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/standard-integration/server/python/requirements.txt b/standard-integration/server/python/requirements.txt new file mode 100644 index 00000000..361d7b0b --- /dev/null +++ b/standard-integration/server/python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.3 +paypal-server-sdk==1.0.0 \ No newline at end of file diff --git a/standard-integration/server/python/server.py b/standard-integration/server/python/server.py new file mode 100644 index 00000000..0c379a55 --- /dev/null +++ b/standard-integration/server/python/server.py @@ -0,0 +1,96 @@ +import logging +import os + +from flask import Flask, request, Response +from paypalserversdk.http.auth.o_auth_2 import ClientCredentialsAuthCredentials +from paypalserversdk.logging.configuration.api_logging_configuration import ( + LoggingConfiguration, + RequestLoggingConfiguration, + ResponseLoggingConfiguration, +) +from paypalserversdk.paypal_serversdk_client import PaypalServersdkClient +from paypalserversdk.controllers.orders_controller import OrdersController +from paypalserversdk.models.amount_with_breakdown import AmountWithBreakdown +from paypalserversdk.models.checkout_payment_intent import CheckoutPaymentIntent +from paypalserversdk.models.order_request import OrderRequest +from paypalserversdk.models.purchase_unit_request import PurchaseUnitRequest +from paypalserversdk.api_helper import ApiHelper + +app = Flask(__name__) + +paypal_client: PaypalServersdkClient = PaypalServersdkClient( + client_credentials_auth_credentials=ClientCredentialsAuthCredentials( + o_auth_client_id=os.getenv("PAYPAL_CLIENT_ID"), + o_auth_client_secret=os.getenv("PAYPAL_CLIENT_SECRET"), + ), + logging_configuration=LoggingConfiguration( + log_level=logging.INFO, + # Disable masking of sensitive headers for Sandbox testing. + # This should be set to True (the default if unset)in production. + mask_sensitive_headers=False, + request_logging_config=RequestLoggingConfiguration( + log_headers=True, log_body=True + ), + response_logging_config=ResponseLoggingConfiguration( + log_headers=True, log_body=True + ), + ), +) + +""" +Health check +""" + + +@app.route("/", methods=["GET"]) +def index(): + return {"message": "Server is running"} + + +orders_controller: OrdersController = paypal_client.orders + +""" +Create an order to start the transaction. + +@see https://developer.paypal.com/docs/api/orders/v2/#orders_create +""" + + +@app.route("/api/orders", methods=["POST"]) +def create_order(): + request_body = request.get_json() + # use the cart information passed from the front-end to calculate the order amount detals + cart = request_body["cart"] + order = orders_controller.create_order( + { + "body": OrderRequest( + intent=CheckoutPaymentIntent.CAPTURE, + purchase_units=[ + PurchaseUnitRequest( + AmountWithBreakdown(currency_code="USD", value="100.00") + ) + ], + ), + "prefer": "return=representation", + } + ) + return Response( + ApiHelper.json_serialize(order.body), status=200, mimetype="application/json" + ) + + +""" + Capture payment for the created order to complete the transaction. + + @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture +""" + + +@app.route("/api/orders//capture", methods=["POST"]) +def capture_order(order_id): + order = orders_controller.capture_order( + {"id": order_id, "prefer": "return=representation"} + ) + return Response( + ApiHelper.json_serialize(order.body), status=200, mimetype="application/json" + ) diff --git a/standard-integration/server/ruby/.ruby-version b/standard-integration/server/ruby/.ruby-version new file mode 100644 index 00000000..fa7adc7a --- /dev/null +++ b/standard-integration/server/ruby/.ruby-version @@ -0,0 +1 @@ +3.3.5 diff --git a/standard-integration/server/ruby/Gemfile b/standard-integration/server/ruby/Gemfile new file mode 100644 index 00000000..8772983f --- /dev/null +++ b/standard-integration/server/ruby/Gemfile @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "paypal-server-sdk", "~> 1.0.0" +gem "puma", "~> 6.4" +gem "rackup", "~> 2.1" +gem "sinatra", "~> 4.0" +gem "sinatra-contrib", "~> 4.0" \ No newline at end of file diff --git a/standard-integration/server/ruby/Gemfile.lock b/standard-integration/server/ruby/Gemfile.lock new file mode 100644 index 00000000..64193644 --- /dev/null +++ b/standard-integration/server/ruby/Gemfile.lock @@ -0,0 +1,123 @@ +GEM + remote: https://rubygems.org/ + specs: + apimatic_core (0.3.13) + apimatic_core_interfaces (~> 0.2.0) + certifi (~> 2018.1, >= 2018.01.18) + faraday-multipart (~> 1.0) + nokogiri (~> 1.13, >= 1.13.10) + apimatic_core_interfaces (0.2.1) + apimatic_faraday_client_adapter (0.1.4) + apimatic_core_interfaces (~> 0.2.0) + certifi (~> 2018.1, >= 2018.01.18) + faraday (~> 2.0, >= 2.0.1) + faraday-follow_redirects (~> 0.2) + faraday-gzip (~> 1.0) + faraday-http-cache (~> 2.2) + faraday-multipart (~> 1.0) + faraday-net_http_persistent (~> 2.0) + faraday-retry (~> 2.0) + base64 (0.2.0) + certifi (2018.01.18) + connection_pool (2.5.0) + faraday (2.12.2) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) + faraday-gzip (1.0.0) + faraday (>= 1.0) + zlib (~> 2.1) + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) + faraday-net_http_persistent (2.3.0) + faraday (~> 2.5) + net-http-persistent (>= 4.0.4, < 5) + faraday-retry (2.2.1) + faraday (~> 2.0) + json (2.10.2) + logger (1.6.6) + multi_json (1.15.0) + multipart-post (2.4.1) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) + net-http (0.6.0) + uri + net-http-persistent (4.0.5) + connection_pool (~> 2.2) + nio4r (2.7.4) + nokogiri (1.18.5-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.5-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.5-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.5-x86_64-linux-musl) + racc (~> 1.4) + paypal-server-sdk (1.0.0) + apimatic_core (~> 0.3.11) + apimatic_core_interfaces (~> 0.2.1) + apimatic_faraday_client_adapter (~> 0.1.4) + puma (6.6.0) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.1.12) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rackup (2.2.1) + rack (>= 3) + ruby2_keywords (0.0.5) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) + sinatra-contrib (4.1.1) + multi_json (>= 0.0.2) + mustermann (~> 3.0) + rack-protection (= 4.1.1) + sinatra (= 4.1.1) + tilt (~> 2.0) + tilt (2.6.0) + uri (1.0.3) + zlib (2.1.1) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + paypal-server-sdk (~> 1.0.0) + puma (~> 6.4) + rackup (~> 2.1) + sinatra (~> 4.0) + sinatra-contrib (~> 4.0) + +BUNDLED WITH + 2.5.18 diff --git a/standard-integration/server/ruby/README.md b/standard-integration/server/ruby/README.md new file mode 100644 index 00000000..6ad18fe2 --- /dev/null +++ b/standard-integration/server/ruby/README.md @@ -0,0 +1,37 @@ +# Standard Integration Ruby Sinatra Sample + +PayPal Standard Integration sample in Ruby using Sinatra + +## Running the sample + +1. **Ensure you have a supported Ruby version installed**: [Ruby Maintenance Branches](https://www.ruby-lang.org/en/downloads/branches/) + +1. **Install the dependencies** + + ```bash + bundle install + ``` + +1. **Add your API credentials to the environment:** + + - **Windows** + + ```powershell + $env:PAYPAL_CLIENT_ID = "" + $env:PAYPAL_CLIENT_SECRET = "" + ``` + + - **Unix** + + ```bash + export PAYPAL_CLIENT_ID="" + export PAYPAL_CLIENT_SECRET="" + ``` + +1. **Run the server** + + ```bash + bundle exec ruby server.rb + ``` + +1. Go to [http://localhost:8080/](http://localhost:8080/) diff --git a/standard-integration/server/ruby/server.rb b/standard-integration/server/ruby/server.rb new file mode 100644 index 00000000..b87bace7 --- /dev/null +++ b/standard-integration/server/ruby/server.rb @@ -0,0 +1,67 @@ +require 'paypal_server_sdk' +require 'sinatra' +require 'sinatra/json' + +include PaypalServerSdk + +set :port, 8080 + +paypal_client = PaypalServerSdk::Client.new( + client_credentials_auth_credentials: ClientCredentialsAuthCredentials.new( + o_auth_client_id: ENV['PAYPAL_CLIENT_ID'], + o_auth_client_secret: ENV['PAYPAL_CLIENT_SECRET'] + ), + environment: Environment::SANDBOX, + logging_configuration: LoggingConfiguration.new( + mask_sensitive_headers: false, + log_level: Logger::INFO, + request_logging_config: RequestLoggingConfiguration.new( + log_headers: true, + log_body: true, + ), + response_logging_config: ResponseLoggingConfiguration.new( + log_headers: true, + log_body: true + ) + ) +) + +# Health Check +get '/' do + json :message => "Server is running" +end + +# Create an order to start the transaction. +# +# @see https://developer.paypal.com/docs/api/orders/v2/#orders_create +post "/api/orders" do + # use the cart information passed from the front-end to calculate the order amount detals + cart = JSON.parse request.body.read + order_response = paypal_client.orders.create_order({ + 'body' => OrderRequest.new( + intent: CheckoutPaymentIntent::CAPTURE, + purchase_units: [ + PurchaseUnitRequest.new( + amount: AmountWithBreakdown.new( + currency_code: 'USD', + value: '100.00' + ) + ) + ] + ), + 'prefer' => 'return=representation' + }) + json order_response.data +end + +# Capture payment for the created order to complete the transaction. +# +# @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture +post '/api/orders/:order_id/capture' do |order_id| + capture_response = paypal_client.orders.capture_order({ + 'id' => order_id, + 'prefer' => 'return=representation' + }) + json capture_response.data +rescue ErrorException => e +end \ No newline at end of file