├── .devcontainer ├── angular_dotnet │ └── devcontainer.json ├── angular_java │ └── devcontainer.json ├── angular_node │ └── devcontainer.json ├── angular_php │ └── devcontainer.json ├── angular_python │ └── devcontainer.json ├── angular_ruby │ └── devcontainer.json ├── html_dotnet │ └── devcontainer.json ├── html_java │ └── devcontainer.json ├── html_node │ └── devcontainer.json ├── html_php │ └── devcontainer.json ├── html_python │ └── devcontainer.json ├── html_ruby │ └── devcontainer.json ├── update_settings.sh ├── vue_dotnet │ └── devcontainer.json ├── vue_java │ └── devcontainer.json ├── vue_node │ └── devcontainer.json ├── vue_php │ └── devcontainer.json ├── vue_python │ └── devcontainer.json └── vue_ruby │ └── devcontainer.json ├── .gitignore ├── LICENSE ├── README.md ├── client ├── angular │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── karma.conf.js │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── components │ │ │ │ ├── billing │ │ │ │ │ ├── billing.component.html │ │ │ │ │ └── billing.component.ts │ │ │ │ ├── customer │ │ │ │ │ ├── customer.component.html │ │ │ │ │ └── customer.component.ts │ │ │ │ ├── payment-flexible │ │ │ │ │ ├── payment-flexible.component.html │ │ │ │ │ └── payment-flexible.component.ts │ │ │ │ ├── payment │ │ │ │ │ ├── payment.component.html │ │ │ │ │ └── payment.component.ts │ │ │ │ └── shipping │ │ │ │ │ ├── shipping.component.html │ │ │ │ │ └── shipping.component.ts │ │ │ ├── interfaces │ │ │ │ ├── types.ts │ │ │ │ └── window.interface.ts │ │ │ ├── pages │ │ │ │ ├── checkout-flexible │ │ │ │ │ ├── checkout-flexible.component.html │ │ │ │ │ └── checkout-flexible.component.ts │ │ │ │ ├── checkout-switcher │ │ │ │ │ ├── checkout-switcher.component.html │ │ │ │ │ └── checkout-switcher.component.ts │ │ │ │ └── checkout │ │ │ │ │ ├── checkout.component.html │ │ │ │ │ └── checkout.component.ts │ │ │ └── services │ │ │ │ ├── sdk.service.ts │ │ │ │ ├── transaction.service.ts │ │ │ │ └── window.service.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── html │ ├── README.md │ └── src │ │ ├── init-fastlane-flexible.js │ │ ├── init-fastlane.js │ │ └── styles.css └── vue │ ├── .env.development │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierrc.json │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── jsconfig.json │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── assets │ │ └── main.css │ ├── main.js │ ├── router │ │ └── index.js │ ├── utils │ │ └── form.js │ └── views │ │ ├── CheckoutFlexibleView.vue │ │ ├── CheckoutSwitcher.vue │ │ └── CheckoutView.vue │ └── vite.config.js └── server ├── .env.example ├── dotnet ├── Controllers │ └── ServerController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── README.md ├── TemplateResolver.cs ├── appsettings.json └── dotnet.csproj ├── java ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── fastlane │ │ └── paypalsample │ │ ├── JavaApplication.java │ │ ├── ServletInitializer.java │ │ └── sample │ │ ├── ServerController.java │ │ └── models │ │ ├── Address.java │ │ ├── AuthResponse.java │ │ ├── ClientTokenResponse.java │ │ ├── Name.java │ │ ├── PaymentToken.java │ │ ├── PhoneNumber.java │ │ ├── Request.java │ │ └── ShippingAddress.java │ └── resources │ └── application.properties ├── node ├── README.md ├── package.json └── src │ └── server.js ├── php ├── .gitignore ├── README.md ├── bin │ └── console ├── composer.json ├── composer.lock ├── config │ ├── bundles.php │ ├── packages │ │ ├── cache.yaml │ │ ├── framework.yaml │ │ ├── nelmio_cors.yaml │ │ └── routing.yaml │ ├── preload.php │ ├── routes.php │ ├── routes │ │ └── framework.yaml │ └── services.yaml ├── public │ └── index.php ├── src │ ├── Controller │ │ └── ServerController.php │ └── Kernel.php └── symfony.lock ├── python ├── README.md ├── requirements.txt └── server.py ├── ruby ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md └── src │ └── server.rb └── shared └── views ├── checkout-flexible.html └── checkout.html /.devcontainer/angular_dotnet/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + Dotnet", 3 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 5000, 7 | 5001, 8 | 4200 9 | ], 10 | "portsAttributes": { 11 | "5000": { 12 | "label": "Dotnet HTTP", 13 | "onAutoForward": "openBrowserOnce" 14 | }, 15 | "5001": { 16 | "label": "Dotnet HTTPS", 17 | "onAutoForward": "openBrowserOnce" 18 | }, 19 | "4200": { 20 | "label": "Angular", 21 | "onAutoForward": "openBrowserOnce" 22 | } 23 | }, 24 | "secrets": { 25 | "PAYPAL_CLIENT_ID": { 26 | "description": "Sandbox client ID of the application.", 27 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 28 | }, 29 | "PAYPAL_CLIENT_SECRET": { 30 | "description": "Sandbox secret of the application.", 31 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 32 | } 33 | }, 34 | "containerEnv": { 35 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 36 | "DOMAINS": "paypal.com", 37 | "VISIBLE_FOLDER_SERVER": "dotnet", 38 | "VISIBLE_FOLDER_CLIENT": "angular" 39 | }, 40 | "customizations": { 41 | "vscode": { 42 | "extensions": [ 43 | "vsls-contrib.codetour", 44 | "PayPal.vscode-paypal", 45 | "ms-dotnettools.csharp" 46 | ] 47 | }, 48 | "settings": { 49 | "git.openRepositoryInParentFolders": "always" 50 | }, 51 | "codespaces": { 52 | "openFiles": [ 53 | "README.md" 54 | ] 55 | } 56 | }, 57 | "features": { 58 | "ghcr.io/devcontainers/features/node:1": { 59 | "version": "lts" 60 | } 61 | }, 62 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/dotnet && dotnet restore && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 63 | "postAttachCommand": "cd server/dotnet && dotnet run & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 64 | } -------------------------------------------------------------------------------- /.devcontainer/angular_java/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + Java", 3 | "image": "mcr.microsoft.com/devcontainers/java:17", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080, 7 | 4200 8 | ], 9 | "portsAttributes": { 10 | "8080": { 11 | "label": "Java", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "4200": { 15 | "label": "Angular", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "java", 33 | "VISIBLE_FOLDER_CLIENT": "angular" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "vscjava.vscode-java-pack", 41 | "angular.ng-template" 42 | ] 43 | }, 44 | "settings": { 45 | "git.openRepositoryInParentFolders": "always" 46 | }, 47 | "codespaces": { 48 | "openFiles": [ 49 | "README.md" 50 | ] 51 | } 52 | }, 53 | "features": { 54 | "ghcr.io/devcontainers/features/java:1": { 55 | "version": "17", 56 | "installMaven": "true" 57 | }, 58 | "ghcr.io/devcontainers/features/node:1": { 59 | "version": "lts" 60 | } 61 | }, 62 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch java/.env && cd java && mvn clean install && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 63 | "postAttachCommand": "cd /workspaces/${localWorkspaceFolderBasename}/server/java && mvn spring-boot:run & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 64 | } -------------------------------------------------------------------------------- /.devcontainer/angular_node/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + Node", 3 | "image": "mcr.microsoft.com/devcontainers/javascript-node:20", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 3000, 7 | 4200 8 | ], 9 | "portsAttributes": { 10 | "3000": { 11 | "label": "Node", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "4200": { 15 | "label": "Angular", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "node", 33 | "VISIBLE_FOLDER_CLIENT": "angular" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "dbaeumer.vscode-eslint" 41 | ] 42 | }, 43 | "settings": { 44 | "git.openRepositoryInParentFolders": "always" 45 | }, 46 | "codespaces": { 47 | "openFiles": [ 48 | "README.md" 49 | ] 50 | } 51 | }, 52 | "features": { 53 | "ghcr.io/devcontainers/features/node:1": { 54 | "version": "lts" 55 | } 56 | }, 57 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/node && npm install && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 58 | "postAttachCommand": "cd server/node && npm start & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 59 | } -------------------------------------------------------------------------------- /.devcontainer/angular_php/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + PHP", 3 | "image": "mcr.microsoft.com/devcontainers/php:8", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080, 7 | 4200 8 | ], 9 | "portsAttributes": { 10 | "8080": { 11 | "label": "PHP", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "4200": { 15 | "label": "Angular", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 33 | "VISIBLE_FOLDER_SERVER": "php", 34 | "VISIBLE_FOLDER_CLIENT": "angular" 35 | }, 36 | "customizations": { 37 | "vscode": { 38 | "extensions": [ 39 | "vsls-contrib.codetour", 40 | "PayPal.vscode-paypal", 41 | "xdebug.php-debug" 42 | ] 43 | }, 44 | "settings": { 45 | "git.openRepositoryInParentFolders": "always" 46 | }, 47 | "codespaces": { 48 | "openFiles": [ 49 | "README.md" 50 | ] 51 | } 52 | }, 53 | "features": { 54 | "ghcr.io/devcontainers/features/node:1": { 55 | "version": "lts" 56 | } 57 | }, 58 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch php/.env && cd php && composer install && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 59 | "postAttachCommand": "cd server/php && php -S localhost:8080 -t public/ & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 60 | } -------------------------------------------------------------------------------- /.devcontainer/angular_python/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + Python", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.10", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8888, 7 | 4200 8 | ], 9 | "portsAttributes": { 10 | "8888": { 11 | "label": "Python", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "4200": { 15 | "label": "Angular", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "python", 33 | "VISIBLE_FOLDER_CLIENT": "angular" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "ms-python.python" 41 | ] 42 | }, 43 | "settings": { 44 | "git.openRepositoryInParentFolders": "always", 45 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" 46 | }, 47 | "codespaces": { 48 | "openFiles": [ 49 | "README.md" 50 | ] 51 | } 52 | }, 53 | "features": { 54 | "ghcr.io/devcontainers/features/node:1": { 55 | "version": "lts" 56 | } 57 | }, 58 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/python && python -m venv .venv && . .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 59 | "postAttachCommand": "cd server/python && . .venv/bin/activate && python server.py & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 60 | } -------------------------------------------------------------------------------- /.devcontainer/angular_ruby/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Angular + Ruby", 3 | "image": "mcr.microsoft.com/devcontainers/ruby:2.7", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 4567, 7 | 4200 8 | ], 9 | "portsAttributes": { 10 | "4567": { 11 | "label": "Ruby", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "4200": { 15 | "label": "Angular", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 33 | "VISIBLE_FOLDER_SERVER": "ruby", 34 | "VISIBLE_FOLDER_CLIENT": "angular" 35 | }, 36 | "customizations": { 37 | "vscode": { 38 | "extensions": [ 39 | "vsls-contrib.codetour", 40 | "PayPal.vscode-paypal", 41 | "rebornix.Ruby" 42 | ] 43 | }, 44 | "settings": { 45 | "git.openRepositoryInParentFolders": "always" 46 | }, 47 | "codespaces": { 48 | "openFiles": [ 49 | "README.md" 50 | ] 51 | } 52 | }, 53 | "features": { 54 | "ghcr.io/devcontainers/features/node:1": { 55 | "version": "lts" 56 | } 57 | }, 58 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/ruby && gem install bundler:1.17.2 && bundle _1.17.2_ install --path vendor/bundle && cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm install", 59 | "postAttachCommand": "cd server/ruby && bundle exec ruby src/server.rb & cd /workspaces/${localWorkspaceFolderBasename}/client/angular && npm start --no-analytics" 60 | } -------------------------------------------------------------------------------- /.devcontainer/html_dotnet/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + Dotnet", 3 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 5000, 7 | 5001 8 | ], 9 | "portsAttributes": { 10 | "5000": { 11 | "label": "Dotnet HTTP", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5001": { 15 | "label": "Dotnet HTTPS", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "dotnet", 33 | "VISIBLE_FOLDER_CLIENT": "html" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "ms-dotnettools.csharp" 41 | ] 42 | }, 43 | "settings": { 44 | "git.openRepositoryInParentFolders": "always" 45 | }, 46 | "codespaces": { 47 | "openFiles": [ 48 | "README.md" 49 | ] 50 | } 51 | }, 52 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/dotnet && dotnet restore", 53 | "postAttachCommand": "cd server/dotnet && dotnet run" 54 | } -------------------------------------------------------------------------------- /.devcontainer/html_java/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + Java", 3 | "image": "mcr.microsoft.com/devcontainers/java:17", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080 7 | ], 8 | "portsAttributes": { 9 | "8080": { 10 | "label": "Java", 11 | "onAutoForward": "openBrowserOnce" 12 | } 13 | }, 14 | "secrets": { 15 | "PAYPAL_CLIENT_ID": { 16 | "description": "Sandbox client ID of the application.", 17 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 18 | }, 19 | "PAYPAL_CLIENT_SECRET": { 20 | "description": "Sandbox secret of the application.", 21 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 22 | } 23 | }, 24 | "containerEnv": { 25 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 26 | "DOMAINS": "paypal.com", 27 | "VISIBLE_FOLDER_SERVER": "java", 28 | "VISIBLE_FOLDER_CLIENT": "html" 29 | }, 30 | "customizations": { 31 | "vscode": { 32 | "extensions": [ 33 | "vsls-contrib.codetour", 34 | "PayPal.vscode-paypal", 35 | "vscjava.vscode-java-pack" 36 | ] 37 | }, 38 | "settings": { 39 | "git.openRepositoryInParentFolders": "always" 40 | }, 41 | "codespaces": { 42 | "openFiles": [ 43 | "README.md" 44 | ] 45 | } 46 | }, 47 | "features": { 48 | "ghcr.io/devcontainers/features/java:1": { 49 | "version": "17", 50 | "installMaven": "true" 51 | } 52 | }, 53 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch java/.env && cd java && mvn clean install", 54 | "postAttachCommand": "cd server/java && mvn spring-boot:run" 55 | } -------------------------------------------------------------------------------- /.devcontainer/html_node/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + Node", 3 | "image": "mcr.microsoft.com/devcontainers/javascript-node:20", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 3000 7 | ], 8 | "portsAttributes": { 9 | "3000": { 10 | "label": "Node", 11 | "onAutoForward": "openBrowserOnce" 12 | } 13 | }, 14 | "secrets": { 15 | "PAYPAL_CLIENT_ID": { 16 | "description": "Sandbox client ID of the application.", 17 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 18 | }, 19 | "PAYPAL_CLIENT_SECRET": { 20 | "description": "Sandbox secret of the application.", 21 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 22 | } 23 | }, 24 | "containerEnv": { 25 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 26 | "DOMAINS": "paypal.com", 27 | "VISIBLE_FOLDER_SERVER": "node", 28 | "VISIBLE_FOLDER_CLIENT": "html" 29 | }, 30 | "customizations": { 31 | "vscode": { 32 | "extensions": [ 33 | "vsls-contrib.codetour", 34 | "PayPal.vscode-paypal", 35 | "dbaeumer.vscode-eslint" 36 | ] 37 | }, 38 | "settings": { 39 | "git.openRepositoryInParentFolders": "always" 40 | }, 41 | "codespaces": { 42 | "openFiles": [ 43 | "README.md" 44 | ] 45 | } 46 | }, 47 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/node && npm install", 48 | "postAttachCommand": "cd server/node && npm start" 49 | } -------------------------------------------------------------------------------- /.devcontainer/html_php/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + PHP", 3 | "image": "mcr.microsoft.com/devcontainers/php:8", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080 7 | ], 8 | "portsAttributes": { 9 | "8080": { 10 | "label": "PHP", 11 | "onAutoForward": "openBrowserOnce" 12 | } 13 | }, 14 | "secrets": { 15 | "PAYPAL_CLIENT_ID": { 16 | "description": "Sandbox client ID of the application.", 17 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 18 | }, 19 | "PAYPAL_CLIENT_SECRET": { 20 | "description": "Sandbox secret of the application.", 21 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 22 | } 23 | }, 24 | "containerEnv": { 25 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 26 | "DOMAINS": "paypal.com", 27 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 28 | "VISIBLE_FOLDER_SERVER": "php", 29 | "VISIBLE_FOLDER_CLIENT": "html" 30 | }, 31 | "customizations": { 32 | "vscode": { 33 | "extensions": [ 34 | "vsls-contrib.codetour", 35 | "PayPal.vscode-paypal", 36 | "xdebug.php-debug" 37 | ] 38 | }, 39 | "settings": { 40 | "git.openRepositoryInParentFolders": "always" 41 | }, 42 | "codespaces": { 43 | "openFiles": [ 44 | "README.md" 45 | ] 46 | } 47 | }, 48 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch php/.env && cd php && composer install", 49 | "postAttachCommand": "cd server/php && php -S localhost:8080 -t public/" 50 | } -------------------------------------------------------------------------------- /.devcontainer/html_python/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + Python", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.10", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8888 7 | ], 8 | "portsAttributes": { 9 | "8888": { 10 | "label": "Python", 11 | "onAutoForward": "openBrowserOnce" 12 | } 13 | }, 14 | "secrets": { 15 | "PAYPAL_CLIENT_ID": { 16 | "description": "Sandbox client ID of the application.", 17 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 18 | }, 19 | "PAYPAL_CLIENT_SECRET": { 20 | "description": "Sandbox secret of the application.", 21 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 22 | } 23 | }, 24 | "containerEnv": { 25 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 26 | "DOMAINS": "paypal.com", 27 | "VISIBLE_FOLDER_SERVER": "python", 28 | "VISIBLE_FOLDER_CLIENT": "html" 29 | }, 30 | "customizations": { 31 | "vscode": { 32 | "extensions": [ 33 | "vsls-contrib.codetour", 34 | "PayPal.vscode-paypal", 35 | "ms-python.python" 36 | ] 37 | }, 38 | "settings": { 39 | "git.openRepositoryInParentFolders": "always", 40 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" 41 | }, 42 | "codespaces": { 43 | "openFiles": [ 44 | "README.md" 45 | ] 46 | } 47 | }, 48 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/python && python -m venv .venv && . .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt", 49 | "postAttachCommand": "cd server/python && . .venv/bin/activate && python server.py" 50 | } -------------------------------------------------------------------------------- /.devcontainer/html_ruby/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML + Ruby", 3 | "image": "mcr.microsoft.com/devcontainers/ruby:2.7", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 4567 7 | ], 8 | "portsAttributes": { 9 | "4567": { 10 | "label": "Ruby", 11 | "onAutoForward": "openBrowserOnce" 12 | } 13 | }, 14 | "secrets": { 15 | "PAYPAL_CLIENT_ID": { 16 | "description": "Sandbox client ID of the application.", 17 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 18 | }, 19 | "PAYPAL_CLIENT_SECRET": { 20 | "description": "Sandbox secret of the application.", 21 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 22 | } 23 | }, 24 | "containerEnv": { 25 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 26 | "DOMAINS": "paypal.com", 27 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 28 | "VISIBLE_FOLDER_SERVER": "ruby", 29 | "VISIBLE_FOLDER_CLIENT": "html" 30 | }, 31 | "customizations": { 32 | "vscode": { 33 | "extensions": [ 34 | "vsls-contrib.codetour", 35 | "PayPal.vscode-paypal", 36 | "rebornix.Ruby" 37 | ] 38 | }, 39 | "settings": { 40 | "git.openRepositoryInParentFolders": "always" 41 | }, 42 | "codespaces": { 43 | "openFiles": [ 44 | "README.md" 45 | ] 46 | } 47 | }, 48 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/ruby && gem install bundler:1.17.2 && bundle _1.17.2_ install --path vendor/bundle", 49 | "postAttachCommand": "cd server/ruby && bundle exec ruby src/server.rb" 50 | } -------------------------------------------------------------------------------- /.devcontainer/update_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | VISIBLE_FOLDER_SERVER="$VISIBLE_FOLDER_SERVER" 6 | VISIBLE_FOLDER_CLIENT="$VISIBLE_FOLDER_CLIENT" 7 | VISIBLE_FOLDER_DEVCONTAINER="${VISIBLE_FOLDER_CLIENT}_${VISIBLE_FOLDER_SERVER}" 8 | 9 | if [ -z "$VISIBLE_FOLDER_CLIENT" ]; then 10 | echo "Error: VISIBLE_FOLDER_CLIENT is not set, setting it to default" 11 | VISIBLE_FOLDER_CLIENT="DEFAULT" 12 | fi 13 | 14 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 15 | WORKSPACE_DIR="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )" 16 | SERVER_DIR="$WORKSPACE_DIR/server" 17 | CLIENT_DIR="$WORKSPACE_DIR/client" 18 | DEVCONTATINER_DIR="$WORKSPACE_DIR/.devcontainer" 19 | SETTINGS_FILE="$WORKSPACE_DIR/.vscode/settings.json" 20 | 21 | echo "Workspace directory: $WORKSPACE_DIR" 22 | echo "Server directory: $SERVER_DIR" 23 | echo "Visible server folder: $VISIBLE_FOLDER_SERVER" 24 | echo "Visible client folder: $VISIBLE_FOLDER_CLIENT" 25 | 26 | if [ ! -d "$SERVER_DIR" ]; then 27 | echo "Error: Server directory not found at $SERVER_DIR" 28 | exit 1 29 | fi 30 | 31 | if [ ! -d "$DEVCONTATINER_DIR" ]; then 32 | echo "Error: .devcontainer directory not found at $DEVCONTATINER_DIR" 33 | exit 1 34 | fi 35 | 36 | if [ -z "$VISIBLE_FOLDER_SERVER" ]; then 37 | echo "Error: VISIBLE_FOLDER_SERVER is not set" 38 | exit 1 39 | fi 40 | 41 | mkdir -p "$(dirname "$SETTINGS_FILE")" 42 | 43 | echo "{ 44 | \"files.exclude\": {" > "$SETTINGS_FILE" 45 | 46 | first=true 47 | for dir in "$SERVER_DIR"/*/ ; do 48 | dir_name=$(basename "$dir") 49 | if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_SERVER" ] && ([ "$dir_name" != "shared" ] || [ "$VISIBLE_FOLDER_CLIENT" != "html" ]); then 50 | if [ "$first" = true ] ; then 51 | first=false 52 | else 53 | echo "," >> "$SETTINGS_FILE" 54 | fi 55 | echo -n " \"**/server/$dir_name\": true" >> "$SETTINGS_FILE" 56 | fi 57 | done 58 | 59 | for dir in "$DEVCONTATINER_DIR"/*/ ; do 60 | dir_name=$(basename "$dir") 61 | if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_SERVER" ] && [ "$dir_name" != "$VISIBLE_FOLDER_DEVCONTAINER" ]; then 62 | if [ "$first" = true ] ; then 63 | first=false 64 | else 65 | echo "," >> "$SETTINGS_FILE" 66 | fi 67 | echo -n " \"**/.devcontainer/$dir_name\": true" >> "$SETTINGS_FILE" 68 | fi 69 | done 70 | 71 | for dir in "$CLIENT_DIR"/*/ ; do 72 | dir_name=$(basename "$dir") 73 | if [ -d "$dir" ] && [ "$dir_name" != "$VISIBLE_FOLDER_CLIENT" ]; then 74 | if [ "$first" = true ] ; then 75 | first=false 76 | else 77 | echo "," >> "$SETTINGS_FILE" 78 | fi 79 | echo -n " \"**/client/$dir_name\": true" >> "$SETTINGS_FILE" 80 | fi 81 | done 82 | 83 | echo " 84 | } 85 | }" >> "$SETTINGS_FILE" 86 | 87 | echo "VS Code settings updated to show only $VISIBLE_FOLDER_SERVER and $VISIBLE_FOLDER_CLIENT folder in server directory." 88 | echo "Contents of $SETTINGS_FILE:" 89 | cat "$SETTINGS_FILE" -------------------------------------------------------------------------------- /.devcontainer/vue_dotnet/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + Dotnet", 3 | "image": "mcr.microsoft.com/devcontainers/dotnet:8.0", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 5000, 7 | 5001, 8 | 5173 9 | ], 10 | "portsAttributes": { 11 | "5000": { 12 | "label": "Dotnet HTTP", 13 | "onAutoForward": "openBrowserOnce" 14 | }, 15 | "5001": { 16 | "label": "Dotnet HTTPS", 17 | "onAutoForward": "openBrowserOnce" 18 | }, 19 | "5173": { 20 | "label": "Vue", 21 | "onAutoForward": "openBrowserOnce" 22 | } 23 | }, 24 | "secrets": { 25 | "PAYPAL_CLIENT_ID": { 26 | "description": "Sandbox client ID of the application.", 27 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 28 | }, 29 | "PAYPAL_CLIENT_SECRET": { 30 | "description": "Sandbox secret of the application.", 31 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 32 | } 33 | }, 34 | "containerEnv": { 35 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 36 | "DOMAINS": "paypal.com", 37 | "VISIBLE_FOLDER_SERVER": "dotnet", 38 | "VISIBLE_FOLDER_CLIENT": "vue" 39 | }, 40 | "customizations": { 41 | "vscode": { 42 | "extensions": [ 43 | "vsls-contrib.codetour", 44 | "PayPal.vscode-paypal", 45 | "ms-dotnettools.csharp", 46 | "Vue.volar", 47 | "Vue.vscode-typescript-vue-plugin" 48 | ] 49 | }, 50 | "settings": { 51 | "git.openRepositoryInParentFolders": "always" 52 | }, 53 | "codespaces": { 54 | "openFiles": [ 55 | "README.md" 56 | ] 57 | } 58 | }, 59 | "features": { 60 | "ghcr.io/devcontainers/features/node:1": { 61 | "version": "lts" 62 | } 63 | }, 64 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/dotnet && dotnet restore && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 65 | "postAttachCommand": "cd server/dotnet && dotnet run & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 66 | } -------------------------------------------------------------------------------- /.devcontainer/vue_java/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + Java", 3 | "image": "mcr.microsoft.com/devcontainers/java:17", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080, 7 | 5173 8 | ], 9 | "portsAttributes": { 10 | "8080": { 11 | "label": "Java", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5173": { 15 | "label": "Vue", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "java", 33 | "VISIBLE_FOLDER_CLIENT": "vue" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "vscjava.vscode-java-pack", 41 | "Vue.volar", 42 | "Vue.vscode-typescript-vue-plugin" 43 | ] 44 | }, 45 | "settings": { 46 | "git.openRepositoryInParentFolders": "always" 47 | }, 48 | "codespaces": { 49 | "openFiles": [ 50 | "README.md" 51 | ] 52 | } 53 | }, 54 | "features": { 55 | "ghcr.io/devcontainers/features/java:1": { 56 | "version": "17", 57 | "installMaven": "true" 58 | }, 59 | "ghcr.io/devcontainers/features/node:1": { 60 | "version": "lts" 61 | } 62 | }, 63 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch java/.env && cd java && mvn clean install && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 64 | "postAttachCommand": "cd server/java && mvn spring-boot:run & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 65 | } -------------------------------------------------------------------------------- /.devcontainer/vue_node/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + Node", 3 | "image": "mcr.microsoft.com/devcontainers/javascript-node:20", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 3000, 7 | 5173 8 | ], 9 | "portsAttributes": { 10 | "3000": { 11 | "label": "Node", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5173": { 15 | "label": "Vue", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "node", 33 | "VISIBLE_FOLDER_CLIENT": "vue" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "dbaeumer.vscode-eslint", 41 | "Vue.volar", 42 | "Vue.vscode-typescript-vue-plugin" 43 | ] 44 | }, 45 | "settings": { 46 | "git.openRepositoryInParentFolders": "always" 47 | }, 48 | "codespaces": { 49 | "openFiles": [ 50 | "README.md" 51 | ] 52 | } 53 | }, 54 | "features": { 55 | "ghcr.io/devcontainers/features/node:1": { 56 | "version": "lts" 57 | } 58 | }, 59 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/node && npm install && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 60 | "postAttachCommand": "cd server/node && npm start & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 61 | } -------------------------------------------------------------------------------- /.devcontainer/vue_php/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + PHP", 3 | "image": "mcr.microsoft.com/devcontainers/php:8", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8080, 7 | 5173 8 | ], 9 | "portsAttributes": { 10 | "8080": { 11 | "label": "PHP", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5173": { 15 | "label": "Vue", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 33 | "VISIBLE_FOLDER_SERVER": "php", 34 | "VISIBLE_FOLDER_CLIENT": "vue" 35 | }, 36 | "customizations": { 37 | "vscode": { 38 | "extensions": [ 39 | "vsls-contrib.codetour", 40 | "PayPal.vscode-paypal", 41 | "xdebug.php-debug", 42 | "Vue.volar", 43 | "Vue.vscode-typescript-vue-plugin" 44 | ] 45 | }, 46 | "settings": { 47 | "git.openRepositoryInParentFolders": "always" 48 | }, 49 | "codespaces": { 50 | "openFiles": [ 51 | "README.md" 52 | ] 53 | } 54 | }, 55 | "features": { 56 | "ghcr.io/devcontainers/features/node:1": { 57 | "version": "lts" 58 | } 59 | }, 60 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd /workspaces/${localWorkspaceFolderBasename}/server && touch php/.env && cd php && composer install && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 61 | "postAttachCommand": "cd server/php && php -S localhost:8080 -t public/ & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 62 | } -------------------------------------------------------------------------------- /.devcontainer/vue_python/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + Python", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.10", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 8888, 7 | 5173 8 | ], 9 | "portsAttributes": { 10 | "8888": { 11 | "label": "Python", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5173": { 15 | "label": "Vue", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "VISIBLE_FOLDER_SERVER": "python", 33 | "VISIBLE_FOLDER_CLIENT": "vue" 34 | }, 35 | "customizations": { 36 | "vscode": { 37 | "extensions": [ 38 | "vsls-contrib.codetour", 39 | "PayPal.vscode-paypal", 40 | "ms-python.python", 41 | "Vue.volar", 42 | "Vue.vscode-typescript-vue-plugin" 43 | ] 44 | }, 45 | "settings": { 46 | "git.openRepositoryInParentFolders": "always", 47 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" 48 | }, 49 | "codespaces": { 50 | "openFiles": [ 51 | "README.md" 52 | ] 53 | } 54 | }, 55 | "features": { 56 | "ghcr.io/devcontainers/features/node:1": { 57 | "version": "lts" 58 | } 59 | }, 60 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/python && python -m venv .venv && . .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 61 | "postAttachCommand": "cd server/python && . .venv/bin/activate && python server.py & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 62 | } -------------------------------------------------------------------------------- /.devcontainer/vue_ruby/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue + Ruby", 3 | "image": "mcr.microsoft.com/devcontainers/ruby:2.7", 4 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 5 | "forwardPorts": [ 6 | 4567, 7 | 5173 8 | ], 9 | "portsAttributes": { 10 | "4567": { 11 | "label": "Ruby", 12 | "onAutoForward": "openBrowserOnce" 13 | }, 14 | "5173": { 15 | "label": "Vue", 16 | "onAutoForward": "openBrowserOnce" 17 | } 18 | }, 19 | "secrets": { 20 | "PAYPAL_CLIENT_ID": { 21 | "description": "Sandbox client ID of the application.", 22 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 23 | }, 24 | "PAYPAL_CLIENT_SECRET": { 25 | "description": "Sandbox secret of the application.", 26 | "documentationUrl": "https://developer.paypal.com/dashboard/applications/sandbox" 27 | } 28 | }, 29 | "containerEnv": { 30 | "PAYPAL_API_BASE_URL": "https://api-m.sandbox.paypal.com", 31 | "DOMAINS": "paypal.com", 32 | "PAYPAL_SDK_BASE_URL": "https://www.sandbox.paypal.com", 33 | "VISIBLE_FOLDER_SERVER": "ruby", 34 | "VISIBLE_FOLDER_CLIENT": "vue" 35 | }, 36 | "customizations": { 37 | "vscode": { 38 | "extensions": [ 39 | "vsls-contrib.codetour", 40 | "PayPal.vscode-paypal", 41 | "rebornix.Ruby", 42 | "Vue.volar", 43 | "Vue.vscode-typescript-vue-plugin" 44 | ] 45 | }, 46 | "settings": { 47 | "git.openRepositoryInParentFolders": "always" 48 | }, 49 | "codespaces": { 50 | "openFiles": [ 51 | "README.md" 52 | ] 53 | } 54 | }, 55 | "features": { 56 | "ghcr.io/devcontainers/features/node:1": { 57 | "version": "lts" 58 | } 59 | }, 60 | "postCreateCommand": "chmod +x .devcontainer/update_settings.sh && .devcontainer/update_settings.sh && cd server/ruby && gem install bundler:1.17.2 && bundle _1.17.2_ install --path vendor/bundle && cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm install", 61 | "postAttachCommand": "cd server/ruby && bundle exec ruby src/server.rb & cd /workspaces/${localWorkspaceFolderBasename}/client/vue && npm run dev" 62 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | 3 | ### MacOS ### 4 | .DS_Store 5 | 6 | ### Node.js ### 7 | node_modules/ 8 | package-lock.json 9 | 10 | ### .NET ### 11 | **.sln 12 | **.http 13 | **/dotnet/**/bin/ 14 | obj/ 15 | .fake 16 | 17 | ### Java ### 18 | HELP.md 19 | target/ 20 | !.mvn/wrapper/maven-wrapper.jar 21 | !**/src/main/**/target/ 22 | !**/src/test/**/target/ 23 | 24 | ### STS ### 25 | .apt_generated 26 | .classpath 27 | .factorypath 28 | .project 29 | .settings 30 | .springBeans 31 | .sts4-cache 32 | 33 | ### IntelliJ IDEA ### 34 | .idea 35 | *.iws 36 | *.iml 37 | *.ipr 38 | 39 | ### NetBeans ### 40 | /nbproject/private/ 41 | /nbbuild/ 42 | /dist/ 43 | /nbdist/ 44 | /.nb-gradle/ 45 | build/ 46 | !**/src/main/**/build/ 47 | !**/src/test/**/build/ 48 | 49 | ### Python ### 50 | **/venv**/ 51 | **/__pycache__/ 52 | 53 | ### Ruby ### 54 | *.gem 55 | *.rbc 56 | /.config 57 | /coverage/ 58 | /InstalledFiles 59 | /pkg/ 60 | /spec/reports/ 61 | /spec/examples.txt 62 | /test/tmp/ 63 | /test/version_tmp/ 64 | /tmp/ 65 | .byebug_history 66 | .dat* 67 | .repl_history 68 | build/ 69 | *.bridgesupport 70 | build-iPhoneOS/ 71 | build-iPhoneSimulator/ 72 | /.yardoc/ 73 | /_yardoc/ 74 | /doc/ 75 | /rdoc/ 76 | /.bundle/ 77 | /vendor/bundle 78 | /lib/bundler/man/ 79 | .rvmrc 80 | 81 | ### Devcontainer ### 82 | .vscode/settings.json 83 | .devcontainer/update_settings.sh 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastlane Sample Application 2 | 3 | This sample app demonstrates how to integrate with Fastlane using PayPal's REST APIs. 4 | 5 | ## Before You Code 6 | 7 | 1. **Setup a PayPal Account** 8 | 9 | To get started, you'll need a developer, personal, or business account. 10 | 11 | [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) 12 | 13 | You'll then need to visit the [Developer Dashboard](https://developer.paypal.com/dashboard/) to obtain credentials and to make sandbox accounts. 14 | 15 | 2. **Create an Application** 16 | 17 | 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). 18 | 19 | ## How to Run Locally 20 | 21 | 1. Clone the repository by running the following command in your terminal: 22 | ``` 23 | git clone https://github.com/paypal-examples/fastlane-sample-application.git 24 | ``` 25 | 2. Copy the `.env.example` file from the `server` folder and paste it into the folder for the server technology you want to use as `.env`. For example (substitute `node` for the technology of your choice): 26 | ``` 27 | cd fastlane-sample-application/server 28 | cp .env.example node/.env 29 | cd node 30 | ``` 31 | To run this application, you will need this folder and the `shared` folder. The other folders under `server` can be safely deleted or ignored. 32 | 3. Open the `.env` file in a text editor and replace the placeholders with the appropriate values. 33 | 4. To run the server, follow the instructions in your chosen folder's README. 34 | 5. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. 35 | 36 | ## Client Integrations 37 | 38 | ### Quick Start Integration 39 | 40 | #### Overview 41 | Fastlane Quick Start Integration uses PayPal's pre-built UI for payment collection, thereby allowing you to integrate quickly and easily. The Fastlane Payment Component will automatically render the following: 42 | 1. Customer's selected card and "Change" link which allows users to choose a different saved card or use a new card (for Fastlane members) 43 | 2. Credit card and billing address fields (for Guest users or for Fastlane members who don't have an accepted card in their profile) 44 | 45 | #### Key Features 46 | - Quickest way to integrate Fastlane 47 | - Pre-formatted display to show Fastlane members their selected payment method 48 | - Payment form including billing address for Guest users 49 | - Data Security: Quick Start Integration is PCI DSS compliant, ensuring that customer payment information is handled securely 50 | 51 | ### Flexible Integration 52 | 53 | #### Overview 54 | Fastlane Flexible Integration allows you to customize and style your payment page according to the look and feel of your website. The Fastlane Card Component renders input fields for Guest users to enter their credit card details, while the Card Selector provides an interface for Fastlane members to choose a different saved card or use a new card. You will be responsible for: 55 | 1. Showing the selected payment method, the Fastlane watermark, and a button to open the Card Selector (for Fastlane members) 56 | 2. Collecting billing address information and rendering the Fastlane Card Component (for Guest users and Fastlane members who don't have an accepted card in their profile) 57 | 58 | #### Key Features 59 | - Further customize the behavior and experience of your checkout 60 | - Data Security: Flexible Integration is PCI DSS compliant, ensuring that customer payment information is handled securely 61 | 62 | # Running on Codespaces 63 | Follow below steps to use Codespaces. 64 | 65 | 1) Click "New with options..." to open a page where you can choose the Dev container to run. 66 |  67 | 68 | 2) Choose the Dev container to run 69 |  70 | 71 | 3) Client ID and Client Secrets are required for running the application in codespace. 72 |  73 | 74 | 75 | 76 | ### Link to codespaces 77 | 78 | | Application | Codespaces Link | 79 | | ---- | ---- | 80 | | Angular + Dotnet | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_dotnet%2Fdevcontainer.json)| 81 | | Angular + Java | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_java%2Fdevcontainer.json)| 82 | | Angular + Node | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_node%2Fdevcontainer.json)| 83 | | Angular + PHP | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_php%2Fdevcontainer.json)| 84 | | Angular + Python | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_python%2Fdevcontainer.json)| 85 | | Angular + Ruby | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fangular_ruby%2Fdevcontainer.json)| 86 | | HTML + Dotnet | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_dotnet%2Fdevcontainer.json)| 87 | | HTML + Java | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_java%2Fdevcontainer.json)| 88 | | HTML + Node | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_node%2Fdevcontainer.json)| 89 | | HTML + PHP | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_php%2Fdevcontainer.json)| 90 | | HTML + Python | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_python%2Fdevcontainer.json)| 91 | | HTML + Ruby | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fhtml_ruby%2Fdevcontainer.json)| 92 | | Vue + Dotnet | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_dotnet%2Fdevcontainer.json)| 93 | | Vue + Java | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_java%2Fdevcontainer.json)| 94 | | Vue + Node | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_node%2Fdevcontainer.json)| 95 | | Vue + PHP | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_php%2Fdevcontainer.json)| 96 | | Vue + Python | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_python%2Fdevcontainer.json)| 97 | | Vue + Ruby | [](https://codespaces.new/paypal-examples/fastlane-sample-application?devcontainer_path=.devcontainer%2Fvue_ruby%2Fdevcontainer.json)| 98 | 99 | 100 | ### Learn more 101 | 102 | You can read more about codespaces in the [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces). 103 | 104 | ### Feedback 105 | 106 | * To report a bug or suggest a new feature, create an [issue in GitHub](https://github.com/paypal-examples/paypaldevsupport/issues/new/choose). 107 | * To submit feedback, go to [PayPal Developer Docs](https://developer.paypal.com/api/rest/sandbox/codespaces) and select the "Feedback" tab 108 | -------------------------------------------------------------------------------- /client/angular/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/angular/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /client/angular/README.md: -------------------------------------------------------------------------------- 1 | # ClientAngular 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.13. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /client/angular/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "client-angular": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": { 17 | "base": "dist/", 18 | "browser": "" 19 | }, 20 | "index": "src/index.html", 21 | "polyfills": ["src/polyfills.ts"], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": ["src/favicon.ico", "src/assets"], 24 | "styles": ["src/styles.css"], 25 | "scripts": [], 26 | "browser": "src/main.ts" 27 | }, 28 | "configurations": { 29 | "production": { 30 | "budgets": [ 31 | { 32 | "type": "initial", 33 | "maximumWarning": "500kb", 34 | "maximumError": "1mb" 35 | }, 36 | { 37 | "type": "anyComponentStyle", 38 | "maximumWarning": "5kb", 39 | "maximumError": "8kb" 40 | } 41 | ], 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "outputHashing": "all" 49 | }, 50 | "development": { 51 | "optimization": false, 52 | "extractLicenses": false, 53 | "sourceMap": true, 54 | "namedChunks": true 55 | } 56 | }, 57 | "defaultConfiguration": "production" 58 | }, 59 | "serve": { 60 | "builder": "@angular-devkit/build-angular:dev-server", 61 | "configurations": { 62 | "production": { 63 | "buildTarget": "client-angular:build:production" 64 | }, 65 | "development": { 66 | "buildTarget": "client-angular:build:development" 67 | } 68 | }, 69 | "defaultConfiguration": "development" 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n", 73 | "options": { 74 | "buildTarget": "client-angular:build" 75 | } 76 | }, 77 | "test": { 78 | "builder": "@angular-devkit/build-angular:karma", 79 | "options": { 80 | "main": "src/test.ts", 81 | "polyfills": "src/polyfills.ts", 82 | "tsConfig": "tsconfig.spec.json", 83 | "karmaConfig": "karma.conf.js", 84 | "assets": ["src/favicon.ico", "src/assets"], 85 | "styles": ["src/styles.css"], 86 | "scripts": [] 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/angular/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true, // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/client-angular'), 29 | subdir: '.', 30 | reporters: [{ type: 'html' }, { type: 'text-summary' }], 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false, 39 | restartOnFileChange: true, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /client/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^18.2.0", 14 | "@angular/common": "^18.2.0", 15 | "@angular/compiler": "^18.2.0", 16 | "@angular/core": "^18.2.0", 17 | "@angular/forms": "^18.2.0", 18 | "@angular/platform-browser": "^18.2.0", 19 | "@angular/platform-browser-dynamic": "^18.2.0", 20 | "@angular/router": "^18.2.0", 21 | "rxjs": "~7.5.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.14.10" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^18.2.1", 27 | "@angular/cli": "~18.2.1", 28 | "@angular/compiler-cli": "^18.2.0", 29 | "@types/jasmine": "~4.0.0", 30 | "jasmine-core": "~4.3.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.1.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.4.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/angular/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { CheckoutSwitcherComponent } from './pages/checkout-switcher/checkout-switcher.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '**', 8 | component: CheckoutSwitcherComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule { } 17 | -------------------------------------------------------------------------------- /client/angular/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | /* Sample application styles for demo purposes -- not required for Fastlane */ 2 | 3 | body { 4 | background-color: #faf8f5; 5 | font-family: 'paypal-open', sans-serif, system-ui; 6 | } 7 | 8 | button { 9 | border-radius: 4px; 10 | cursor: pointer; 11 | font-weight: bold; 12 | transition: 13 | color 0.2s ease, 14 | background-color 0.2s ease, 15 | border-color 0.2s ease; 16 | } 17 | 18 | button:disabled { 19 | cursor: wait; 20 | } 21 | 22 | fieldset { 23 | border: none; 24 | display: flex; 25 | flex-direction: column; 26 | gap: 16px; 27 | margin: 16px 0; 28 | padding: 0; 29 | } 30 | 31 | form { 32 | box-sizing: border-box; 33 | margin: 0 auto; 34 | padding: 32px; 35 | width: min(640px, 100%); 36 | } 37 | 38 | h1 { 39 | font-size: 24px; 40 | font-weight: normal; 41 | line-height: 30px; 42 | margin: 0; 43 | text-align: center; 44 | } 45 | 46 | h2 { 47 | color: #001435; 48 | font-size: 1.75rem; 49 | font-weight: normal; 50 | line-height: 28px; 51 | margin: 0; 52 | } 53 | 54 | hr { 55 | border: 1px solid #dbdde0; 56 | width: 100%; 57 | } 58 | 59 | hr:has(+ section[hidden]) { 60 | display: none; 61 | } 62 | 63 | input[type='checkbox'] { 64 | margin: 8px; 65 | transform: scale(2); 66 | } 67 | 68 | input[type='checkbox']:focus { 69 | border: 1.5px solid #0070e0; 70 | outline: none; 71 | } 72 | 73 | input:not([type='checkbox']) { 74 | position: relative; 75 | width: 100%; 76 | min-height: 4rem; 77 | border: #929496 solid 0.0625rem; 78 | border-radius: 0.25rem; 79 | box-sizing: border-box; 80 | font-size: 1.125rem; 81 | line-height: 1.5rem; 82 | color: #001435; 83 | margin: 0; 84 | padding: 1.75rem 0.6875rem 0.625rem 0.6875rem; 85 | } 86 | 87 | input:not([type='checkbox'])::placeholder { 88 | color: transparent; 89 | } 90 | 91 | input:not([type='checkbox']):focus { 92 | border: 1.5px solid #0070e0; 93 | box-shadow: 0 0 0 0.125rem #ffffff; 94 | outline-offset: 0.125rem; 95 | outline: 0.125rem solid #097ff5; 96 | } 97 | 98 | input:focus + .label, 99 | input:not(:placeholder-shown) + .label { 100 | top: 0.5rem; 101 | font-size: 0.875rem; 102 | line-height: 1.25rem; 103 | } 104 | 105 | section { 106 | margin: 32px 0 16px 0; 107 | font-size: 1.125rem; 108 | font-weight: 400; 109 | } 110 | 111 | section.active .edit-button:not(.pinned) { 112 | display: none; 113 | } 114 | 115 | section.active .summary { 116 | display: none; 117 | } 118 | 119 | section.pinned .edit-button:not(.pinned) { 120 | display: none; 121 | } 122 | 123 | section.pinned .summary { 124 | display: none; 125 | } 126 | 127 | section:not(.active, .pinned) fieldset { 128 | display: none; 129 | } 130 | 131 | section:not(.active, .pinned) .submit-button { 132 | display: none; 133 | } 134 | 135 | section:not(.visited) .edit-button { 136 | display: none; 137 | } 138 | 139 | *:has(#shipping-required-checkbox:not(:checked)) ~ .form-row { 140 | display: none; 141 | } 142 | 143 | .button-icon { 144 | width: 1rem; 145 | } 146 | 147 | .checkbox-label { 148 | padding-inline-start: 0.5rem; 149 | font-size: 1.125rem; 150 | } 151 | 152 | .edit-button { 153 | display: flex; 154 | align-items: center; 155 | gap: 8px; 156 | 157 | min-width: 3.75rem; 158 | background-color: initial; 159 | border: 2px solid #003087; 160 | box-sizing: border-box; 161 | color: #003087; 162 | font-size: 14px; 163 | font-weight: 700; 164 | height: 32px; 165 | line-height: 20px; 166 | padding: 0px 16px 0px 8px; 167 | 168 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24' data-ppui='true' %0Acolor='%23003087' %3E%3Cpath fill-rule='evenodd' d='M11.278 8.77l.026-.026 5.45-5.451a1 1 0 0 1 1.415 0l2.54 2.54a1 1 0 0 1 0 1.414l-3.93 3.928a.937.937 0 0 1-.025.027l-7.375 7.375a2 2 0 0 1-.965.535l-3.801.877a.5.5 0 0 1-.6-.6l.877-3.8a2 2 0 0 1 .535-.965l5.853-5.854zm3.29-1.169l2.894-2.894 1.832 1.832L16.4 9.434 14.568 7.6z' clip-rule='evenodd' data-ppui='true' %3E%3C/path%3E%3C/svg%3E"); 169 | background-repeat: no-repeat no-repeat; 170 | background-position: left; 171 | background-size: 22px; 172 | background-origin: content-box; 173 | } 174 | 175 | .edit-button:hover:enabled { 176 | color: #0070e0; 177 | border-color: #0070e0; 178 | filter: invert(31%) sepia(95%) saturate(3744%) hue-rotate(197deg) 179 | brightness(95%) contrast(101%); 180 | } 181 | 182 | .email-container { 183 | display: flex; 184 | flex-direction: row; 185 | align-items: center; 186 | gap: 16px; 187 | width: 100%; 188 | } 189 | 190 | .email-input-with-watermark { 191 | flex: auto; 192 | margin: 0; 193 | } 194 | 195 | .email-section { 196 | display: flex; 197 | flex-direction: column; 198 | width: 100%; 199 | } 200 | 201 | .form-row { 202 | display: flex; 203 | gap: 1rem; 204 | } 205 | 206 | .form-group { 207 | position: relative; 208 | flex: 1; 209 | } 210 | 211 | .header { 212 | align-items: center; 213 | display: flex; 214 | justify-content: space-between; 215 | margin: 16px 0; 216 | } 217 | 218 | .label { 219 | position: absolute; 220 | top: 1.25rem; 221 | right: 0.75rem; 222 | left: 0.75rem; 223 | margin-bottom: 0; 224 | 225 | pointer-events: none; 226 | color: #545d68; 227 | background-color: transparent; 228 | background-clip: padding-box; 229 | font-size: 1.125rem; 230 | line-height: 1.5rem; 231 | 232 | transition: all 0.2s; 233 | transition-timing-function: cubic-bezier(0.2, 0, 0, 1); 234 | } 235 | 236 | .summary { 237 | margin: 32px 0 16px 0; 238 | } 239 | 240 | .submit-button:not([hidden]) { 241 | background-color: #003087; 242 | border: none; 243 | color: #ffffff; 244 | display: block; 245 | font-size: 18px; 246 | height: 48px; 247 | line-height: 24px; 248 | margin: 0 auto; 249 | padding: 12px 32px 12px 32px; 250 | } 251 | 252 | .submit-button:not([hidden]):hover:enabled { 253 | background-color: #0070e0; 254 | border-color: #0070e0; 255 | } 256 | 257 | .submit-button:not(#email-submit-button) { 258 | border-radius: 1000px; 259 | margin-top: 32px; 260 | } 261 | 262 | .submit-button:disabled { 263 | background-color: #dbdde0; 264 | } 265 | 266 | #payment:not(.active, .pinned) ~ #checkout-button { 267 | display: none; 268 | } 269 | 270 | #watermark-container { 271 | align-self: flex-end; 272 | min-height: 24px; 273 | margin-right: 155px; 274 | } 275 | 276 | .input-invalid { 277 | border: 1.5px solid #e00034; 278 | outline: 0.125rem solid #f5093e; 279 | } 280 | -------------------------------------------------------------------------------- /client/angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'client-angular'; 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; 4 | 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { CheckoutComponent } from './pages/checkout/checkout.component'; 8 | import { WINDOW_PROVIDER } from './services/window.service'; 9 | import { CustomerComponent } from './components/customer/customer.component'; 10 | import { ReactiveFormsModule } from '@angular/forms'; 11 | import { PaymentComponent } from './components/payment/payment.component'; 12 | import { ShippingComponent } from './components/shipping/shipping.component'; 13 | import { CheckoutFlexibleComponent } from './pages/checkout-flexible/checkout-flexible.component'; 14 | import { BillingComponent } from './components/billing/billing.component'; 15 | import { PaymentFlexibleComponent } from './components/payment-flexible/payment-flexible.component'; 16 | import { CheckoutSwitcherComponent } from './pages/checkout-switcher/checkout-switcher.component'; 17 | 18 | @NgModule({ 19 | declarations: [ 20 | AppComponent, 21 | CheckoutComponent, 22 | CheckoutFlexibleComponent, 23 | CheckoutSwitcherComponent, 24 | CustomerComponent, 25 | PaymentComponent, 26 | PaymentFlexibleComponent, 27 | ShippingComponent, 28 | BillingComponent 29 | ], 30 | bootstrap: [AppComponent], imports: [BrowserModule, 31 | AppRoutingModule, 32 | ReactiveFormsModule], providers: [ 33 | WINDOW_PROVIDER, 34 | provideHttpClient(withInterceptorsFromDi()) 35 | ] 36 | }) 37 | export class AppModule { } 38 | -------------------------------------------------------------------------------- /client/angular/src/app/components/billing/billing.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Billing 4 | 5 | 6 | Edit 7 | 8 | 9 | 10 | 11 | {{ 12 | billingAddressData.addressLine1 + ', ' + billingAddressData.addressLine2 13 | }} 14 | {{ 15 | billingAddressData.adminArea2 + 16 | ', ' + 17 | billingAddressData.adminArea1 + 18 | ' ' + 19 | billingAddressData.postalCode + 20 | ', ' + 21 | billingAddressData.countryCode 22 | }} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | Address Line 1 36 | 37 | 38 | 39 | 40 | 43 | 44 | Apt., ste, bldg. (optional) 45 | 46 | 47 | 48 | 49 | 50 | 56 | City 57 | 58 | 59 | 65 | State 66 | 67 | 68 | 69 | 70 | 76 | ZIP Code 77 | 78 | 79 | 85 | 86 | Country (eg. US, UK) 87 | 88 | 89 | 90 | 91 | 92 | Continue 93 | 94 | 95 | -------------------------------------------------------------------------------- /client/angular/src/app/components/billing/billing.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; 2 | import { FormGroup, FormControl } from '@angular/forms'; 3 | import { ComponentFormState } from 'src/app/interfaces/types'; 4 | 5 | export interface BillingAddressData { 6 | addressLine1: string, 7 | addressLine2: string, 8 | adminArea2: string, 9 | adminArea1: string, 10 | postalCode: string, 11 | countryCode: string, 12 | }; 13 | 14 | @Component({ 15 | selector: 'app-billing', 16 | templateUrl: './billing.component.html', 17 | styleUrls: ['../../app.component.css'] 18 | }) 19 | export class BillingComponent { 20 | 21 | @ViewChild("billingFormElement") 22 | public billingFormElement!: ElementRef; 23 | 24 | @Input() 25 | public set billingAddressData(billing: BillingAddressData | undefined) { 26 | this.billingForm.reset(); 27 | this.updateBillingForm(billing); 28 | this._billingAddressData = billing; 29 | }; 30 | 31 | public get billingAddressData(): BillingAddressData | undefined { 32 | return this._billingAddressData 33 | } 34 | 35 | public billingFormState = ComponentFormState.Valid; 36 | 37 | public get billingFormInvalid(): boolean { 38 | return this.billingFormState === ComponentFormState.Invalid; 39 | } 40 | 41 | @Input() 42 | public set isActive(active: boolean) { 43 | if (active) { 44 | this.visited = true; 45 | } 46 | 47 | this._isActive = active; 48 | } 49 | 50 | public get isActive(): boolean { 51 | return this._isActive; 52 | } 53 | 54 | @Output() 55 | public editClickEvent = new EventEmitter(); 56 | 57 | @Output() 58 | public billingChangeEvent = new EventEmitter(); 59 | 60 | public billingForm = new FormGroup({ 61 | streetAddress: new FormControl(""), 62 | extendedAddress: new FormControl(""), 63 | locality: new FormControl(""), 64 | region: new FormControl(""), 65 | postalCode: new FormControl(""), 66 | countryCodeAlpha2: new FormControl(""), 67 | }); 68 | 69 | public visited = false; 70 | 71 | private _isActive = false; 72 | 73 | private _billingAddressData: BillingAddressData | undefined; 74 | 75 | public onContinueButtonClick(): void { 76 | 77 | if (!this.billingForm.valid) { 78 | this.billingFormState = ComponentFormState.Invalid; 79 | this.billingFormElement.nativeElement.reportValidity(); 80 | return; 81 | } 82 | 83 | const form = this.billingForm.value; 84 | 85 | const billingData: BillingAddressData = { 86 | addressLine1: form.streetAddress || "", 87 | addressLine2: form.extendedAddress || "", 88 | adminArea2: form.locality || "", 89 | adminArea1: form.region || "", 90 | postalCode: form.postalCode || "", 91 | countryCode: form.countryCodeAlpha2 || "" 92 | }; 93 | 94 | this.billingAddressData = billingData; 95 | 96 | this.billingChangeEvent.emit(billingData); 97 | 98 | this.billingFormState = ComponentFormState.Valid; 99 | } 100 | 101 | public updateBillingForm(billingData: BillingAddressData | undefined) { 102 | 103 | if (!billingData) { 104 | return; 105 | } 106 | 107 | const params = { 108 | streetAddress: billingData.addressLine1, 109 | extendedAddress: billingData.addressLine2, 110 | locality: billingData.adminArea2, 111 | region: billingData.adminArea1, 112 | postalCode: billingData.postalCode, 113 | countryCodeAlpha2: billingData.countryCode, 114 | }; 115 | 116 | this.billingForm.setValue(params); 117 | } 118 | 119 | public onEditButtonClick() { 120 | this.editClickEvent.emit(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /client/angular/src/app/components/customer/customer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Customer 4 | 5 | 6 | Edit 7 | 8 | 9 | 10 | {{ currentEmail }} 11 | 12 | 13 | 14 | 17 | E-mail 18 | 19 | 21 | Continue 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /client/angular/src/app/components/customer/customer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { ShippingAddressData } from '../shipping/shipping.component'; 4 | import { BillingAddressData } from '../billing/billing.component'; 5 | import { ComponentFormState } from 'src/app/interfaces/types'; 6 | 7 | export interface CustomerResponse { 8 | authenticated: boolean; 9 | email: string; 10 | name: string; 11 | shippingAddress?: ShippingAddressData; 12 | billingAddress?: BillingAddressData; 13 | paymentToken: { [key: string]: any } | null; 14 | } 15 | 16 | @Component({ 17 | selector: 'app-customer', 18 | templateUrl: './customer.component.html', 19 | styleUrls: ['../../app.component.css'] 20 | }) 21 | export class CustomerComponent implements OnInit { 22 | 23 | @ViewChild('emailInputElement') 24 | public emailInputElement!: ElementRef; 25 | 26 | @Input() 27 | public watermarkComponent: { render: Function } = { 28 | render: () => { } 29 | }; 30 | 31 | @Input() 32 | public identity: { lookupCustomerByEmail: Function; triggerAuthenticationFlow: Function } = { 33 | lookupCustomerByEmail: () => { }, 34 | triggerAuthenticationFlow: () => { } 35 | }; 36 | 37 | @Input() 38 | public set isActive(active: boolean) { 39 | if (active) { 40 | this.visited = true; 41 | this.watermarkComponent.render("#watermark-container"); 42 | } 43 | 44 | this._isActive = active; 45 | } 46 | 47 | @Output() 48 | public editClickEvent = new EventEmitter(); 49 | 50 | @Output() 51 | public emailChangeEvent = new EventEmitter(); 52 | 53 | public visited = true; 54 | 55 | public email: FormControl = new FormControl(""); 56 | 57 | public currentEmail: string = ""; 58 | 59 | public isContinueButtonEnabled = false; 60 | 61 | public customerFormState = ComponentFormState.Valid; 62 | 63 | private _isActive = false; 64 | 65 | public get isActive(): boolean { 66 | return this._isActive; 67 | } 68 | 69 | public get customerFormInvalid(): boolean { 70 | return this.customerFormState === ComponentFormState.Invalid; 71 | } 72 | 73 | public ngOnInit(): void { 74 | this.isContinueButtonEnabled = true; 75 | } 76 | 77 | public onEditButtonClick(): void { 78 | this.editClickEvent.emit(); 79 | } 80 | 81 | public async onContinueButtonClick(event: Event): Promise { 82 | event.preventDefault(); 83 | 84 | if (!this.email.valid) { 85 | this.customerFormState = ComponentFormState.Invalid; 86 | this.emailInputElement.nativeElement.reportValidity(); 87 | return; 88 | } 89 | 90 | this.isContinueButtonEnabled = false; 91 | 92 | this.currentEmail = this.email.value; 93 | 94 | let memberAuthenticatedSuccessfully = false; 95 | let name = undefined; 96 | let shippingAddress = undefined; 97 | let billingAddress = undefined; 98 | let paymentToken = undefined; 99 | 100 | try { 101 | const { customerContextId } = await this.identity.lookupCustomerByEmail(this.currentEmail); 102 | 103 | if (customerContextId) { 104 | const authResponse = await this.identity.triggerAuthenticationFlow(customerContextId); 105 | 106 | console.log("Auth response:", authResponse); 107 | 108 | if (authResponse?.authenticationState === "succeeded") { 109 | 110 | const { profileData } = authResponse; 111 | 112 | memberAuthenticatedSuccessfully = true; 113 | name = profileData.name; 114 | shippingAddress = profileData.shippingAddress; 115 | paymentToken = profileData.card; 116 | billingAddress = paymentToken?.paymentSource.card.billingAddress 117 | } 118 | } else { 119 | console.log("No customerContextId"); 120 | } 121 | 122 | this.emailChangeEvent.emit({ 123 | authenticated: memberAuthenticatedSuccessfully, 124 | email: this.currentEmail, 125 | name, 126 | shippingAddress, 127 | billingAddress, 128 | paymentToken 129 | }); 130 | 131 | this.customerFormState = ComponentFormState.Valid; 132 | } finally { 133 | this.isContinueButtonEnabled = true; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /client/angular/src/app/components/payment-flexible/payment-flexible.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Payment 4 | 6 | 7 | Edit 8 | 9 | 10 | 11 | 12 | {{ cardSummary }} 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/angular/src/app/components/payment-flexible/payment-flexible.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | type PaymentToken = { 4 | paymentSource: { 5 | card: { 6 | lastDigits: string; 7 | } 8 | } 9 | } | { [key: string]: any } | null; 10 | 11 | @Component({ 12 | selector: 'app-payment-flexible', 13 | templateUrl: './payment-flexible.component.html', 14 | styleUrls: ['../../app.component.css'] 15 | }) 16 | export class PaymentFlexibleComponent { 17 | 18 | @Input() 19 | public isAuthenticated = false; 20 | 21 | @Input() 22 | public set paymentToken(token: PaymentToken) { 23 | this.cardSummary = ""; 24 | 25 | if (token) { 26 | this.cardSummary = `💳 •••• ${token.paymentSource.card.lastDigits}` 27 | } 28 | } 29 | 30 | @Input() 31 | public set isActive(active: boolean) { 32 | if (active) { 33 | this.visited = true; 34 | } 35 | 36 | this._isActive = active; 37 | } 38 | 39 | public cardSummary: string = ""; 40 | 41 | public get isActive(): boolean { 42 | return this._isActive; 43 | } 44 | 45 | @Input() 46 | public cardComponent: { render: Function } = { 47 | render: () => { } 48 | }; 49 | 50 | @Output() 51 | public editClickEvent = new EventEmitter(); 52 | 53 | public visited = false; 54 | 55 | private _isActive = false; 56 | 57 | public onEditButtonClick(): void { 58 | this.editClickEvent.emit(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/angular/src/app/components/payment/payment.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Payment 4 | 5 | 6 | Edit 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/angular/src/app/components/payment/payment.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-payment', 5 | templateUrl: './payment.component.html', 6 | styleUrls: ['../../app.component.css'] 7 | }) 8 | export class PaymentComponent { 9 | 10 | @Input() 11 | public isAuthenticated = false; 12 | 13 | @Input() 14 | public set isActive(active: boolean) { 15 | if (active) { 16 | this.visited = true; 17 | } 18 | 19 | if (!this.visited) { 20 | this.paymentComponent.render("#payment-component"); 21 | } 22 | 23 | this._isActive = active; 24 | } 25 | 26 | public get isActive(): boolean { 27 | return this._isActive; 28 | } 29 | 30 | @Input() 31 | public paymentComponent: { render: Function } = { 32 | render: () => { } 33 | }; 34 | 35 | @Output() 36 | public editClickEvent = new EventEmitter(); 37 | 38 | public visited = false; 39 | 40 | private _isActive = false; 41 | 42 | public onEditButtonClick(): void { 43 | this.editClickEvent.emit(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/angular/src/app/components/shipping/shipping.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Shipping 4 | 5 | 6 | Edit 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | This purchase requires shipping 20 | 21 | 22 | 23 | 24 | 29 | First Name 30 | 31 | 32 | 37 | Last Name 38 | 39 | 40 | 41 | 42 | 44 | Company name (optional) 45 | 46 | 47 | 48 | 49 | 54 | Address Line 1 55 | 56 | 57 | 58 | 59 | 61 | 62 | Apt., ste, bldg. (optional) 63 | 64 | 65 | 66 | 67 | 68 | 73 | City 74 | 75 | 76 | 77 | 82 | State 83 | 84 | 85 | 86 | 87 | 93 | ZIP Code 94 | 95 | 96 | 101 | Country (eg. US, UK) 102 | 103 | 104 | 105 | 106 | 113 | 114 | Country calling code 115 | 116 | 117 | 118 | 119 | 125 | Phone Number 126 | 127 | 128 | 129 | 130 | Confirm 131 | 132 | 133 | -------------------------------------------------------------------------------- /client/angular/src/app/components/shipping/shipping.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; 2 | import { FormControl, FormGroup } from '@angular/forms'; 3 | import { ComponentFormState } from 'src/app/interfaces/types'; 4 | 5 | export interface ShippingAddressData { 6 | companyName: string; 7 | address: { 8 | addressLine1: string; 9 | addressLine2: string; 10 | adminArea2: string; 11 | adminArea1: string; 12 | postalCode: string; 13 | countryCode: string; 14 | }, 15 | name: { 16 | firstName: string; 17 | lastName: string; 18 | fullName: string; 19 | }, 20 | phoneNumber: { 21 | countryCode: string; 22 | nationalNumber: string; 23 | }, 24 | }; 25 | 26 | @Component({ 27 | selector: 'app-shipping', 28 | templateUrl: './shipping.component.html', 29 | styleUrls: ['../../app.component.css'] 30 | }) 31 | export class ShippingComponent { 32 | 33 | @ViewChild("shippingFormElement") 34 | public shippingFormElement!: ElementRef; 35 | 36 | @Input() 37 | public isAuthenticated = false; 38 | 39 | @Input() 40 | public set shippingAddressData(shipping: ShippingAddressData | undefined) { 41 | this.shippingForm.reset(); 42 | this.updateShippingForm(shipping); 43 | this._shippingAddressData = shipping; 44 | }; 45 | 46 | public get shippingAddressData(): ShippingAddressData | undefined { 47 | return this._shippingAddressData 48 | } 49 | 50 | @Input() 51 | public set isActive(active: boolean) { 52 | if (active) { 53 | this.visited = true; 54 | } 55 | 56 | this._isActive = active; 57 | } 58 | 59 | public get isActive(): boolean { 60 | return this._isActive; 61 | } 62 | 63 | @Output() 64 | public editClickEvent = new EventEmitter(); 65 | 66 | @Output() 67 | public shippingChangeEvent = new EventEmitter(); 68 | 69 | public shippingFormState = ComponentFormState.Valid; 70 | 71 | public get getAddressSummary(): string { 72 | return this.formatAddressSummary(this.shippingAddressData); 73 | } 74 | 75 | public get shippingFormInvalid(): boolean { 76 | return this.shippingFormState === ComponentFormState.Invalid; 77 | } 78 | 79 | public shippingRequired = new FormControl(true); 80 | 81 | public shippingForm = new FormGroup({ 82 | firstName: new FormControl(""), 83 | lastName: new FormControl(""), 84 | company: new FormControl(""), 85 | streetAddress: new FormControl(""), 86 | extendedAddress: new FormControl(""), 87 | locality: new FormControl(""), 88 | region: new FormControl(""), 89 | postalCode: new FormControl(""), 90 | countryCodeAlpha2: new FormControl(""), 91 | phoneCountryCode: new FormControl(""), 92 | phoneNumber: new FormControl(""), 93 | }); 94 | 95 | public visited = false; 96 | 97 | private _isActive = false; 98 | 99 | private _shippingAddressData: ShippingAddressData | undefined; 100 | 101 | public onContinueButtonClick(): void { 102 | 103 | if (!this.shippingRequired.value) { 104 | this.shippingChangeEvent.emit(undefined); 105 | return; 106 | } 107 | 108 | if (!this.shippingForm.valid) { 109 | this.shippingFormState = ComponentFormState.Invalid; 110 | this.shippingFormElement.nativeElement.reportValidity(); 111 | return; 112 | } 113 | 114 | const form = this.shippingForm.value; 115 | 116 | const shippingData: ShippingAddressData = { 117 | companyName: form.company || "", 118 | address: { 119 | addressLine1: form.streetAddress || "", 120 | addressLine2: form.extendedAddress || "", 121 | adminArea2: form.locality || "", 122 | adminArea1: form.region || "", 123 | postalCode: form.postalCode || "", 124 | countryCode: form.countryCodeAlpha2 || "" 125 | }, 126 | name: { 127 | fullName: `${form.firstName || ""} ${form.lastName || ""}`, 128 | firstName: form.firstName || "", 129 | lastName: form.lastName || "" 130 | }, 131 | phoneNumber: { 132 | countryCode: form.phoneCountryCode || "", 133 | nationalNumber: form.phoneNumber || "" 134 | } 135 | }; 136 | 137 | this.shippingAddressData = shippingData; 138 | 139 | this.shippingChangeEvent.emit(shippingData); 140 | 141 | this.shippingFormState = ComponentFormState.Valid; 142 | } 143 | 144 | public updateShippingForm(shippingData: ShippingAddressData | undefined) { 145 | 146 | const params = { 147 | firstName: shippingData?.name?.firstName || "", 148 | lastName: shippingData?.name?.lastName || "", 149 | company: shippingData?.companyName || "", 150 | streetAddress: shippingData?.address?.addressLine1 || "", 151 | extendedAddress: shippingData?.address?.addressLine2 || "", 152 | locality: shippingData?.address?.adminArea2 || "", 153 | region: shippingData?.address?.adminArea1 || "", 154 | postalCode: shippingData?.address?.postalCode || "", 155 | countryCodeAlpha2: shippingData?.address?.countryCode || "", 156 | phoneCountryCode: shippingData?.phoneNumber?.countryCode || "", 157 | phoneNumber: shippingData?.phoneNumber?.nationalNumber || "" 158 | }; 159 | 160 | this.shippingForm.setValue(params); 161 | } 162 | 163 | public formatAddressSummary(shipping: ShippingAddressData | undefined): string { 164 | if (!shipping) return ""; 165 | const isNotEmpty = (field: any) => Boolean(field); 166 | const summary = [ 167 | shipping.name.fullName || [shipping.name.firstName, shipping.name.lastName].filter(isNotEmpty).join(' '), 168 | shipping.companyName, 169 | [shipping.address.addressLine1, shipping.address.addressLine2].filter(isNotEmpty).join(', '), 170 | [ 171 | shipping.address.adminArea2, 172 | [shipping.address.adminArea1, shipping.address.postalCode].filter(isNotEmpty).join(' '), 173 | shipping.address.countryCode, 174 | ].filter(isNotEmpty).join(', '), 175 | [shipping.phoneNumber.countryCode, shipping.phoneNumber.nationalNumber].filter(isNotEmpty).join(''), 176 | ]; 177 | return summary.filter(isNotEmpty).join(''); 178 | }; 179 | 180 | public onEditButtonClick() { 181 | this.editClickEvent.emit(); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /client/angular/src/app/interfaces/types.ts: -------------------------------------------------------------------------------- 1 | export enum ComponentFormState { Invalid, Valid }; -------------------------------------------------------------------------------- /client/angular/src/app/interfaces/window.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CustomWindow extends Window { 2 | paypal: any; 3 | } 4 | 5 | declare var window: CustomWindow; -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout-flexible/checkout-flexible.component.html: -------------------------------------------------------------------------------- 1 | 2 | Fastlane - PayPal Integration (Flexible) 3 | 4 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | 47 | 48 | 49 | 50 | Checkout 51 | 52 | -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout-flexible/checkout-flexible.component.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Component, ElementRef, Inject, OnInit, Renderer2 } from '@angular/core'; 3 | import { finalize, Observable } from 'rxjs'; 4 | import { lastValueFrom } from 'rxjs/internal/lastValueFrom'; 5 | import { BillingAddressData } from 'src/app/components/billing/billing.component'; 6 | import { CustomerResponse } from 'src/app/components/customer/customer.component'; 7 | import { ShippingAddressData } from 'src/app/components/shipping/shipping.component'; 8 | import { CustomWindow } from 'src/app/interfaces/window.interface'; 9 | import { SDKService } from 'src/app/services/sdk.service'; 10 | import { TransactionService } from 'src/app/services/transaction.service'; 11 | import { WINDOW } from 'src/app/services/window.service'; 12 | 13 | enum Section { 14 | Customer = 'customer', 15 | Shipping = 'shipping', 16 | Billing = 'billing', 17 | Payment = 'payment' 18 | } 19 | 20 | @Component({ 21 | selector: 'app-checkout-flexible', 22 | templateUrl: './checkout-flexible.component.html', 23 | styleUrls: ['../../app.component.css'] 24 | }) 25 | export class CheckoutFlexibleComponent implements OnInit { 26 | 27 | public currentSection = Section.Customer; 28 | 29 | public checkoutButtonEnabled = true; 30 | 31 | public fastlaneProfile: any; 32 | public fastlaneIdentity: any; 33 | public fastlaneCardComponent: any; 34 | public fastlaneWatermarkComponent: any; 35 | public fastlanePaymentWatermarkComponent: any; 36 | 37 | public currentCustomer: CustomerResponse = { 38 | authenticated: false, 39 | name: "", 40 | email: "", 41 | paymentToken: null 42 | }; 43 | 44 | public constructor( 45 | @Inject(WINDOW) 46 | private window: CustomWindow, 47 | @Inject(DOCUMENT) 48 | private _document: Document, 49 | private _renderer2: Renderer2, 50 | private _sdkService: SDKService, 51 | private _el: ElementRef, 52 | private transactionService: TransactionService 53 | ) { } 54 | 55 | public get section(): typeof Section { 56 | return Section; 57 | } 58 | 59 | public async ngOnInit(): Promise { 60 | const initPaypalScriptOb = await this.initPaypalScript(); 61 | 62 | initPaypalScriptOb.subscribe(async () => { 63 | if (!this.window.paypal.Fastlane) { 64 | throw new Error('PayPal script loaded but no Fastlane module'); 65 | } 66 | 67 | const { 68 | identity, 69 | profile, 70 | FastlaneCardComponent, 71 | FastlaneWatermarkComponent, 72 | } = await this.window.paypal.Fastlane({ 73 | styles: { 74 | root: { 75 | backgroundColor: '#faf8f5' 76 | }, 77 | }, 78 | }); 79 | 80 | this.fastlaneIdentity = identity; 81 | this.fastlaneProfile = profile; 82 | this.fastlaneCardComponent = await FastlaneCardComponent(); 83 | this.fastlanePaymentWatermarkComponent = await FastlaneWatermarkComponent({ includeAdditionalInfo: false }); 84 | this.fastlaneWatermarkComponent = await FastlaneWatermarkComponent({ 85 | includeAdditionalInfo: true 86 | }); 87 | }); 88 | } 89 | 90 | 91 | public async initPaypalScript(): Promise> { 92 | const { url: sdkUrl } = await lastValueFrom(this._sdkService.getSDKUrl()); 93 | const { clientToken } = await lastValueFrom(this._sdkService.getSDKClientToken()); 94 | 95 | return new Observable((observer) => { 96 | const script = this._renderer2.createElement('script'); 97 | 98 | script.src = sdkUrl; 99 | script.defer = true; 100 | 101 | script.onload = () => { 102 | observer.next(); 103 | observer.complete(); 104 | }; 105 | 106 | script.onerror = (error: any) => { 107 | observer.error(error); 108 | }; 109 | 110 | this._renderer2.setAttribute(script, 'data-sdk-client-token', clientToken) 111 | this._renderer2.appendChild(this._document.head, script); 112 | }); 113 | } 114 | 115 | public async onCheckoutButtonClick(): Promise { 116 | 117 | this.checkoutButtonEnabled = false; 118 | 119 | const { name, email, shippingAddress, paymentToken: customerPaymentToken, billingAddress } = this.currentCustomer; 120 | 121 | const paymentToken = customerPaymentToken ?? await this.fastlaneCardComponent.getPaymentToken({ billingAddress }); 122 | 123 | this.transactionService 124 | .createTransaction({ 125 | paymentToken, 126 | name, 127 | email, 128 | shippingAddress: shippingAddress ?? undefined 129 | }) 130 | .pipe(finalize(() => { 131 | this.checkoutButtonEnabled = true; 132 | })) 133 | .subscribe((response) => { 134 | const { result, error } = response; 135 | 136 | if (error) { 137 | console.error(error); 138 | return; 139 | } 140 | 141 | if (!result.id) { 142 | console.error(result); 143 | return; 144 | } 145 | 146 | const message = `Order ${result.id}: ${result.status}`; 147 | 148 | console.log(message); 149 | alert(message); 150 | }); 151 | } 152 | 153 | public onEmailChange(nextCustomer: CustomerResponse): void { 154 | this.currentCustomer = nextCustomer; 155 | 156 | this.resetPaymentSection(); 157 | 158 | if (this.currentCustomer.paymentToken) { 159 | this.fastlanePaymentWatermarkComponent.render('#payment-watermark'); 160 | } else { 161 | this.fastlaneCardComponent.render('#card-component'); 162 | } 163 | 164 | if (this.currentCustomer.authenticated && this.currentCustomer.paymentToken) { 165 | this.setActiveSection(Section.Payment); 166 | return; 167 | } 168 | 169 | if (this.currentCustomer.authenticated) { 170 | this.setActiveSection(Section.Billing); 171 | return; 172 | } 173 | 174 | this.setActiveSection(Section.Shipping); 175 | } 176 | 177 | public async onShippingEditButtonClick() { 178 | if (!this.currentCustomer.authenticated) { 179 | this.setActiveSection(Section.Shipping); 180 | return; 181 | } 182 | 183 | const { selectionChanged, selectedAddress } = await this.fastlaneProfile.showShippingAddressSelector(); 184 | 185 | if (selectionChanged) { 186 | this.currentCustomer.shippingAddress = selectedAddress; 187 | } 188 | } 189 | 190 | public async onPaymentEdit() { 191 | if (!this.currentCustomer.paymentToken) { 192 | this.setActiveSection(this.section.Payment); 193 | return; 194 | } 195 | 196 | const { selectionChanged, selectedCard } = await this.fastlaneProfile.showCardSelector(); 197 | 198 | if (selectionChanged) { 199 | this.currentCustomer.paymentToken = selectedCard; 200 | } 201 | } 202 | 203 | public onShippingChange(nextShipping: ShippingAddressData): void { 204 | this.currentCustomer.shippingAddress = nextShipping; 205 | this.setActiveSection(this.currentCustomer.paymentToken ? Section.Payment : Section.Billing); 206 | } 207 | 208 | public onBillingChange(nextBilling: BillingAddressData): void { 209 | this.currentCustomer.billingAddress = nextBilling; 210 | this.setActiveSection(Section.Payment); 211 | } 212 | 213 | public setActiveSection(nextSection: Section): void { 214 | this.currentSection = nextSection; 215 | } 216 | 217 | private resetPaymentSection() { 218 | const paymentWatermark = this._el.nativeElement.querySelector("#payment-watermark"); 219 | const cardComponent = this._el.nativeElement.querySelector("#card-component"); 220 | 221 | paymentWatermark?.replaceChildren(); 222 | cardComponent?.replaceChildren(); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout-switcher/checkout-switcher.component.html: -------------------------------------------------------------------------------- 1 | @if (isFlexible) { 2 | 3 | } @else { 4 | 5 | } -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout-switcher/checkout-switcher.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-checkout-switcher', 6 | templateUrl: './checkout-switcher.component.html', 7 | }) 8 | export class CheckoutSwitcherComponent implements OnInit { 9 | public isFlexible = false; 10 | 11 | constructor(private route: ActivatedRoute) {} 12 | 13 | public ngOnInit(): void { 14 | this.route.queryParams.subscribe(params => { 15 | this.isFlexible = params['flexible'] === 'true'; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout/checkout.component.html: -------------------------------------------------------------------------------- 1 | 2 | Fastlane - PayPal Integration 3 | 4 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | Checkout 38 | 39 | -------------------------------------------------------------------------------- /client/angular/src/app/pages/checkout/checkout.component.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Component, Inject, OnInit, Renderer2 } from '@angular/core'; 3 | import { finalize, Observable } from 'rxjs'; 4 | import { lastValueFrom } from 'rxjs/internal/lastValueFrom'; 5 | import { CustomerResponse } from 'src/app/components/customer/customer.component'; 6 | import { ShippingAddressData } from 'src/app/components/shipping/shipping.component'; 7 | import { CustomWindow } from 'src/app/interfaces/window.interface'; 8 | import { SDKService } from 'src/app/services/sdk.service'; 9 | import { TransactionService } from 'src/app/services/transaction.service'; 10 | import { WINDOW } from 'src/app/services/window.service'; 11 | 12 | enum Section { 13 | Customer = 'customer', 14 | Shipping = 'shipping', 15 | Payment = 'payment' 16 | } 17 | 18 | @Component({ 19 | selector: 'app-checkout', 20 | templateUrl: './checkout.component.html', 21 | styleUrls: ['../../app.component.css'] 22 | }) 23 | export class CheckoutComponent implements OnInit { 24 | 25 | public currentSection = Section.Customer; 26 | 27 | public checkoutButtonEnabled = true; 28 | 29 | public fastlaneProfile: any; 30 | public fastlaneIdentity: any; 31 | public fastlanePaymentComponent: any; 32 | public fastlaneWatermarkComponent: any; 33 | 34 | public currentCustomer: CustomerResponse = { 35 | authenticated: false, 36 | name: "", 37 | email: "", 38 | paymentToken: {} 39 | }; 40 | 41 | public constructor( 42 | @Inject(WINDOW) 43 | private window: CustomWindow, 44 | @Inject(DOCUMENT) 45 | private _document: Document, 46 | private _renderer2: Renderer2, 47 | private _sdkService: SDKService, 48 | private transactionService: TransactionService 49 | ) { } 50 | 51 | public get section(): typeof Section { 52 | return Section; 53 | } 54 | 55 | public async ngOnInit(): Promise { 56 | const initPaypalScriptOb = await this.initPaypalScript(); 57 | 58 | initPaypalScriptOb.subscribe(async () => { 59 | if (!this.window.paypal.Fastlane) { 60 | throw new Error('PayPal script loaded but no Fastlane module'); 61 | } 62 | 63 | const { 64 | identity, 65 | profile, 66 | FastlanePaymentComponent, 67 | FastlaneWatermarkComponent, 68 | } = await this.window.paypal.Fastlane({ 69 | styles: { 70 | root: { 71 | backgroundColor: '#faf8f5' 72 | }, 73 | }, 74 | }); 75 | 76 | this.fastlaneIdentity = identity; 77 | this.fastlaneProfile = profile; 78 | this.fastlanePaymentComponent = await FastlanePaymentComponent(); 79 | this.fastlaneWatermarkComponent = await FastlaneWatermarkComponent({ 80 | includeAdditionalInfo: true 81 | }); 82 | }); 83 | } 84 | 85 | 86 | public async initPaypalScript(): Promise> { 87 | const { url: sdkUrl } = await lastValueFrom(this._sdkService.getSDKUrl()); 88 | const { clientToken } = await lastValueFrom(this._sdkService.getSDKClientToken()); 89 | 90 | return new Observable((observer) => { 91 | const script = this._renderer2.createElement('script'); 92 | 93 | script.src = sdkUrl; 94 | script.defer = true; 95 | 96 | script.onload = () => { 97 | observer.next(); 98 | observer.complete(); 99 | }; 100 | 101 | script.onerror = (error: any) => { 102 | observer.error(error); 103 | }; 104 | 105 | this._renderer2.setAttribute(script, 'data-sdk-client-token', clientToken) 106 | this._renderer2.appendChild(this._document.head, script); 107 | }); 108 | } 109 | 110 | public async onCheckoutButtonClick(): Promise { 111 | const paymentToken = await this.fastlanePaymentComponent.getPaymentToken(); 112 | 113 | this.checkoutButtonEnabled = false; 114 | 115 | const { name, email, shippingAddress } = this.currentCustomer; 116 | 117 | this.transactionService 118 | .createTransaction({ 119 | paymentToken, 120 | name, 121 | email, 122 | shippingAddress: shippingAddress ?? undefined 123 | }) 124 | .pipe(finalize(() => { 125 | this.checkoutButtonEnabled = true; 126 | })) 127 | .subscribe((response) => { 128 | const { result, error } = response; 129 | 130 | if (error) { 131 | console.error(error); 132 | return; 133 | } 134 | 135 | if (!result.id) { 136 | console.error(result); 137 | return; 138 | } 139 | 140 | const message = `Order ${result.id}: ${result.status}`; 141 | 142 | console.log(message); 143 | alert(message); 144 | }); 145 | } 146 | 147 | public onEmailChange(nextCustomer: CustomerResponse): void { 148 | this.currentCustomer = nextCustomer; 149 | 150 | if (this.currentCustomer.authenticated) { 151 | this.setActiveSection(Section.Payment); 152 | } else { 153 | this.setActiveSection(Section.Shipping); 154 | } 155 | } 156 | 157 | public async onShippingEditButtonClick() { 158 | if (!this.currentCustomer.authenticated) { 159 | this.setActiveSection(Section.Shipping); 160 | return; 161 | } 162 | 163 | const { selectionChanged, selectedAddress } = await this.fastlaneProfile.showShippingAddressSelector(); 164 | 165 | if (selectionChanged) { 166 | this.currentCustomer.shippingAddress = selectedAddress; 167 | this.fastlanePaymentComponent.setShippingAddress(this.currentCustomer.shippingAddress); 168 | } 169 | } 170 | 171 | public onShippingChange(nextShipping: ShippingAddressData): void { 172 | this.currentCustomer.shippingAddress = nextShipping; 173 | 174 | if (!nextShipping) { 175 | this.setActiveSection(Section.Payment); 176 | return; 177 | } 178 | 179 | this.fastlanePaymentComponent.setShippingAddress(this.currentCustomer.shippingAddress); 180 | this.setActiveSection(Section.Payment); 181 | } 182 | 183 | public setActiveSection(nextSection: Section): void { 184 | this.currentSection = nextSection; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /client/angular/src/app/services/sdk.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { environment } from 'src/environments/environment'; 5 | 6 | interface SDKUrlResponse { 7 | url: string 8 | }; 9 | 10 | interface SDKClientTokenResponse { 11 | clientToken: string 12 | }; 13 | 14 | @Injectable({ 15 | providedIn: 'root', 16 | }) 17 | export class SDKService { 18 | 19 | constructor(private httpClient: HttpClient) { } 20 | 21 | public getSDKUrl(): Observable { 22 | return this.httpClient.get(`${environment.apiUrl}/sdk/url`); 23 | } 24 | 25 | public getSDKClientToken(): Observable { 26 | return this.httpClient.get(`${environment.apiUrl}/sdk/client-token`); 27 | } 28 | } -------------------------------------------------------------------------------- /client/angular/src/app/services/transaction.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Observable } from 'rxjs'; 4 | import { environment } from 'src/environments/environment'; 5 | import { ShippingAddressData } from '../components/shipping/shipping.component'; 6 | 7 | interface TransactionRequest { 8 | name: string; 9 | email: string; 10 | paymentToken: { [key: string]: any }; 11 | shippingAddress?: ShippingAddressData; 12 | } 13 | 14 | interface TransactionResponse { 15 | result: { id: string; status: string; }; 16 | error?: any; 17 | } 18 | 19 | @Injectable({ 20 | providedIn: 'root', 21 | }) 22 | export class TransactionService { 23 | 24 | constructor(private httpClient: HttpClient) { } 25 | 26 | public createTransaction(payload: TransactionRequest): Observable { 27 | return this.httpClient.post(`${environment.apiUrl}/transaction`, payload); 28 | } 29 | } -------------------------------------------------------------------------------- /client/angular/src/app/services/window.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, Provider } from "@angular/core"; 2 | import { CustomWindow } from "../interfaces/window.interface"; 3 | 4 | export const WINDOW = new InjectionToken('WindowToken'); 5 | 6 | export function windowFactory(): Window { 7 | return window; 8 | } 9 | 10 | export const WINDOW_PROVIDER: Provider = { 11 | provide: WINDOW, 12 | useFactory: windowFactory 13 | } -------------------------------------------------------------------------------- /client/angular/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal-examples/fastlane-sample-application/bd9ba8cabede84c59919fe08791e9cec048a22c1/client/angular/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/angular/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'http://localhost:8080' 4 | }; 5 | -------------------------------------------------------------------------------- /client/angular/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiUrl: 'http://localhost:8080' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /client/angular/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal-examples/fastlane-sample-application/bd9ba8cabede84c59919fe08791e9cec048a22c1/client/angular/src/favicon.ico -------------------------------------------------------------------------------- /client/angular/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fastlane - PayPal Integration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/angular/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /client/angular/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /client/angular/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /client/angular/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /client/angular/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "es2020", 21 | "lib": ["es2020", "dom"], 22 | "useDefineForClassFields": false 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/angular/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /client/html/README.md: -------------------------------------------------------------------------------- 1 | # HTML 2 | 3 | This is a basic HTML/JS implementation of a checkout page integrated with Fastlane. 4 | 5 | ## Local testing 6 | 7 | 1. Make sure that your server of choice is running locally. For more information, see the README at the root of this repository. 8 | 9 | 2. Go to [localhost:8080](localhost:8080) for the Quick Start Integration or [localhost:8080/?flexible](localhost:8080/?flexible) for the Flexible Integration. -------------------------------------------------------------------------------- /client/html/src/styles.css: -------------------------------------------------------------------------------- 1 | /* Sample application styles for demo purposes -- not required for Fastlane */ 2 | 3 | body { 4 | background-color: #faf8f5; 5 | font-family: 'paypal-open', sans-serif, system-ui; 6 | } 7 | 8 | button { 9 | border-radius: 4px; 10 | cursor: pointer; 11 | font-weight: bold; 12 | transition: 13 | color 0.2s ease, 14 | background-color 0.2s ease, 15 | border-color 0.2s ease; 16 | } 17 | 18 | button:disabled { 19 | cursor: wait; 20 | } 21 | 22 | fieldset { 23 | border: none; 24 | display: flex; 25 | flex-direction: column; 26 | gap: 16px; 27 | margin: 16px 0; 28 | padding: 0; 29 | } 30 | 31 | form { 32 | box-sizing: border-box; 33 | margin: 0 auto; 34 | padding: 32px; 35 | width: min(640px, 100%); 36 | } 37 | 38 | h1 { 39 | font-size: 24px; 40 | font-weight: normal; 41 | line-height: 30px; 42 | margin: 0; 43 | text-align: center; 44 | } 45 | 46 | h2 { 47 | color: #001435; 48 | font-size: 1.75rem; 49 | font-weight: normal; 50 | line-height: 28px; 51 | margin: 0; 52 | } 53 | 54 | hr { 55 | border: 1px solid #dbdde0; 56 | width: 100%; 57 | } 58 | 59 | hr:has(+ section[hidden]) { 60 | display: none; 61 | } 62 | 63 | input[type='checkbox'] { 64 | margin: 8px; 65 | transform: scale(2); 66 | } 67 | 68 | input[type='checkbox']:focus { 69 | border: 1.5px solid #0070e0; 70 | outline: none; 71 | } 72 | 73 | input:not([type='checkbox']) { 74 | position: relative; 75 | width: 100%; 76 | min-height: 4rem; 77 | border: #929496 solid 0.0625rem; 78 | border-radius: 0.25rem; 79 | box-sizing: border-box; 80 | font-size: 1.125rem; 81 | line-height: 1.5rem; 82 | color: #001435; 83 | margin: 0; 84 | padding: 1.75rem 0.6875rem 0.625rem 0.6875rem; 85 | } 86 | 87 | input:not([type='checkbox'])::placeholder { 88 | color: transparent; 89 | } 90 | 91 | input:not([type='checkbox']):focus { 92 | border: 1.5px solid #0070e0; 93 | box-shadow: 0 0 0 0.125rem #ffffff; 94 | outline-offset: 0.125rem; 95 | outline: 0.125rem solid #097ff5; 96 | } 97 | 98 | input:focus + .label, 99 | input:not(:placeholder-shown) + .label { 100 | top: 0.5rem; 101 | font-size: 0.875rem; 102 | line-height: 1.25rem; 103 | } 104 | 105 | section { 106 | margin: 32px 0 16px 0; 107 | font-size: 1.125rem; 108 | font-weight: 400; 109 | } 110 | 111 | section.active .edit-button:not(.pinned) { 112 | display: none; 113 | } 114 | 115 | section.active .summary { 116 | display: none; 117 | } 118 | 119 | section.pinned .edit-button:not(.pinned) { 120 | display: none; 121 | } 122 | 123 | section.pinned .summary { 124 | display: none; 125 | } 126 | 127 | section:not(.active, .pinned) fieldset { 128 | display: none; 129 | } 130 | 131 | section:not(.active, .pinned) .submit-button { 132 | display: none; 133 | } 134 | 135 | section:not(.visited) .edit-button { 136 | display: none; 137 | } 138 | 139 | *:has(#shipping-required-checkbox:not(:checked)) ~ .form-row { 140 | display: none; 141 | } 142 | 143 | .button-icon { 144 | width: 1rem; 145 | } 146 | 147 | .checkbox-label { 148 | padding-inline-start: 0.5rem; 149 | font-size: 1.125rem; 150 | } 151 | 152 | .edit-button { 153 | display: flex; 154 | align-items: center; 155 | gap: 8px; 156 | 157 | min-width: 3.75rem; 158 | background-color: initial; 159 | border: 2px solid #003087; 160 | box-sizing: border-box; 161 | color: #003087; 162 | font-size: 14px; 163 | font-weight: 700; 164 | height: 32px; 165 | line-height: 20px; 166 | padding: 0px 16px 0px 8px; 167 | 168 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24' data-ppui='true' %0Acolor='%23003087' %3E%3Cpath fill-rule='evenodd' d='M11.278 8.77l.026-.026 5.45-5.451a1 1 0 0 1 1.415 0l2.54 2.54a1 1 0 0 1 0 1.414l-3.93 3.928a.937.937 0 0 1-.025.027l-7.375 7.375a2 2 0 0 1-.965.535l-3.801.877a.5.5 0 0 1-.6-.6l.877-3.8a2 2 0 0 1 .535-.965l5.853-5.854zm3.29-1.169l2.894-2.894 1.832 1.832L16.4 9.434 14.568 7.6z' clip-rule='evenodd' data-ppui='true' %3E%3C/path%3E%3C/svg%3E"); 169 | background-repeat: no-repeat no-repeat; 170 | background-position: left; 171 | background-size: 22px; 172 | background-origin: content-box; 173 | } 174 | 175 | .edit-button:hover:enabled { 176 | color: #0070e0; 177 | border-color: #0070e0; 178 | filter: invert(31%) sepia(95%) saturate(3744%) hue-rotate(197deg) 179 | brightness(95%) contrast(101%); 180 | } 181 | 182 | .email-container { 183 | display: flex; 184 | flex-direction: row; 185 | align-items: center; 186 | gap: 16px; 187 | width: 100%; 188 | } 189 | 190 | .email-input-with-watermark { 191 | flex: auto; 192 | margin: 0; 193 | } 194 | 195 | .email-section { 196 | display: flex; 197 | flex-direction: column; 198 | width: 100%; 199 | } 200 | 201 | .form-row { 202 | display: flex; 203 | gap: 1rem; 204 | } 205 | 206 | .form-group { 207 | position: relative; 208 | flex: 1; 209 | } 210 | 211 | .header { 212 | align-items: center; 213 | display: flex; 214 | justify-content: space-between; 215 | margin: 16px 0; 216 | } 217 | 218 | .label { 219 | position: absolute; 220 | top: 1.25rem; 221 | right: 0.75rem; 222 | left: 0.75rem; 223 | margin-bottom: 0; 224 | 225 | pointer-events: none; 226 | color: #545d68; 227 | background-color: transparent; 228 | background-clip: padding-box; 229 | font-size: 1.125rem; 230 | line-height: 1.5rem; 231 | 232 | transition: all 0.2s; 233 | transition-timing-function: cubic-bezier(0.2, 0, 0, 1); 234 | } 235 | 236 | .summary { 237 | margin: 32px 0 16px 0; 238 | } 239 | 240 | .submit-button:not([hidden]) { 241 | background-color: #003087; 242 | border: none; 243 | color: #ffffff; 244 | display: block; 245 | font-size: 18px; 246 | height: 48px; 247 | line-height: 24px; 248 | margin: 0 auto; 249 | padding: 12px 32px 12px 32px; 250 | } 251 | 252 | .submit-button:not([hidden]):hover:enabled { 253 | background-color: #0070e0; 254 | border-color: #0070e0; 255 | } 256 | 257 | .submit-button:not(#email-submit-button) { 258 | border-radius: 1000px; 259 | margin-top: 32px; 260 | } 261 | 262 | .submit-button:disabled { 263 | background-color: #dbdde0; 264 | } 265 | 266 | #payment:not(.active, .pinned) ~ #checkout-button { 267 | display: none; 268 | } 269 | 270 | #watermark-container { 271 | align-self: flex-end; 272 | min-height: 24px; 273 | margin-right: 155px; 274 | } 275 | 276 | .input-invalid { 277 | border: 1.5px solid #e00034; 278 | outline: 0.125rem solid #f5093e; 279 | } 280 | -------------------------------------------------------------------------------- /client/vue/.env.development: -------------------------------------------------------------------------------- 1 | VITE_SERVER_URL=http://localhost:8080 -------------------------------------------------------------------------------- /client/vue/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | *.tsbuildinfo 31 | -------------------------------------------------------------------------------- /client/vue/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /client/vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /client/vue/README.md: -------------------------------------------------------------------------------- 1 | # vue 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). 8 | 9 | ## Customize configuration 10 | 11 | See [Vite Configuration Reference](https://vitejs.dev/config/). 12 | 13 | ## Project Setup 14 | 15 | ```sh 16 | npm install 17 | ``` 18 | 19 | ### Compile and Hot-Reload for Development 20 | 21 | ```sh 22 | npm run dev 23 | ``` 24 | 25 | ### Compile and Minify for Production 26 | 27 | ```sh 28 | npm run build 29 | ``` 30 | 31 | ### Lint with [ESLint](https://eslint.org/) 32 | 33 | ```sh 34 | npm run lint 35 | ``` 36 | -------------------------------------------------------------------------------- /client/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fastlane - PayPal Integration 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/vue/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | }, 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /client/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "vue": "^3.4.29", 15 | "vue-router": "^4.3.3", 16 | "@unhead/vue": "^1.9.16" 17 | }, 18 | "devDependencies": { 19 | "@rushstack/eslint-patch": "^1.8.0", 20 | "@vitejs/plugin-vue": "^5.0.5", 21 | "@vue/eslint-config-prettier": "^9.0.0", 22 | "eslint": "^8.57.0", 23 | "eslint-plugin-vue": "^9.23.0", 24 | "prettier": "^3.2.5", 25 | "vite": "^5.3.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal-examples/fastlane-sample-application/bd9ba8cabede84c59919fe08791e9cec048a22c1/client/vue/public/favicon.ico -------------------------------------------------------------------------------- /client/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/vue/src/assets/main.css: -------------------------------------------------------------------------------- 1 | /* Sample application styles for demo purposes -- not required for Fastlane */ 2 | 3 | body { 4 | background-color: #faf8f5; 5 | font-family: 'paypal-open', sans-serif, system-ui; 6 | } 7 | 8 | button { 9 | border-radius: 4px; 10 | cursor: pointer; 11 | font-weight: bold; 12 | transition: 13 | color 0.2s ease, 14 | background-color 0.2s ease, 15 | border-color 0.2s ease; 16 | } 17 | 18 | button:disabled { 19 | cursor: wait; 20 | } 21 | 22 | fieldset { 23 | border: none; 24 | display: flex; 25 | flex-direction: column; 26 | gap: 16px; 27 | margin: 16px 0; 28 | padding: 0; 29 | } 30 | 31 | form { 32 | box-sizing: border-box; 33 | margin: 0 auto; 34 | padding: 32px; 35 | width: min(640px, 100%); 36 | } 37 | 38 | h1 { 39 | font-size: 24px; 40 | font-weight: normal; 41 | line-height: 30px; 42 | margin: 0; 43 | text-align: center; 44 | } 45 | 46 | h2 { 47 | color: #001435; 48 | font-size: 1.75rem; 49 | font-weight: normal; 50 | line-height: 28px; 51 | margin: 0; 52 | } 53 | 54 | hr { 55 | border: 1px solid #dbdde0; 56 | width: 100%; 57 | } 58 | 59 | hr:has(+ section[hidden]) { 60 | display: none; 61 | } 62 | 63 | input[type='checkbox'] { 64 | margin: 8px; 65 | transform: scale(2); 66 | } 67 | 68 | input[type='checkbox']:focus { 69 | border: 1.5px solid #0070e0; 70 | outline: none; 71 | } 72 | 73 | input:not([type='checkbox']) { 74 | position: relative; 75 | width: 100%; 76 | min-height: 4rem; 77 | border: #929496 solid 0.0625rem; 78 | border-radius: 0.25rem; 79 | box-sizing: border-box; 80 | font-size: 1.125rem; 81 | line-height: 1.5rem; 82 | color: #001435; 83 | margin: 0; 84 | padding: 1.75rem 0.6875rem 0.625rem 0.6875rem; 85 | } 86 | 87 | input:not([type='checkbox'])::placeholder { 88 | color: transparent; 89 | } 90 | 91 | input:not([type='checkbox']):focus { 92 | border: 1.5px solid #0070e0; 93 | box-shadow: 0 0 0 0.125rem #ffffff; 94 | outline-offset: 0.125rem; 95 | outline: 0.125rem solid #097ff5; 96 | } 97 | 98 | input:focus + .label, 99 | input:not(:placeholder-shown) + .label { 100 | top: 0.5rem; 101 | font-size: 0.875rem; 102 | line-height: 1.25rem; 103 | } 104 | 105 | section { 106 | margin: 32px 0 16px 0; 107 | font-size: 1.125rem; 108 | font-weight: 400; 109 | white-space: pre; 110 | } 111 | 112 | section.active .edit-button:not(.pinned) { 113 | display: none; 114 | } 115 | 116 | section.active .summary { 117 | display: none; 118 | } 119 | 120 | section.pinned .edit-button:not(.pinned) { 121 | display: none; 122 | } 123 | 124 | section.pinned .summary { 125 | display: none; 126 | } 127 | 128 | section:not(.active, .pinned) fieldset { 129 | display: none; 130 | } 131 | 132 | section:not(.active, .pinned) .submit-button { 133 | display: none; 134 | } 135 | 136 | section:not(.visited) .edit-button { 137 | display: none; 138 | } 139 | 140 | *:has(#shipping-required-checkbox:not(:checked)) ~ .form-row { 141 | display: none; 142 | } 143 | 144 | .button-icon { 145 | width: 1rem; 146 | } 147 | 148 | .checkbox-label { 149 | padding-inline-start: 0.5rem; 150 | font-size: 1.125rem; 151 | } 152 | 153 | .edit-button { 154 | display: flex; 155 | align-items: center; 156 | gap: 8px; 157 | 158 | min-width: 3.75rem; 159 | background-color: initial; 160 | border: 2px solid #003087; 161 | box-sizing: border-box; 162 | color: #003087; 163 | font-size: 14px; 164 | font-weight: 700; 165 | height: 32px; 166 | line-height: 20px; 167 | padding: 0px 16px 0px 8px; 168 | 169 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24' data-ppui='true' %0Acolor='%23003087' %3E%3Cpath fill-rule='evenodd' d='M11.278 8.77l.026-.026 5.45-5.451a1 1 0 0 1 1.415 0l2.54 2.54a1 1 0 0 1 0 1.414l-3.93 3.928a.937.937 0 0 1-.025.027l-7.375 7.375a2 2 0 0 1-.965.535l-3.801.877a.5.5 0 0 1-.6-.6l.877-3.8a2 2 0 0 1 .535-.965l5.853-5.854zm3.29-1.169l2.894-2.894 1.832 1.832L16.4 9.434 14.568 7.6z' clip-rule='evenodd' data-ppui='true' %3E%3C/path%3E%3C/svg%3E"); 170 | background-repeat: no-repeat no-repeat; 171 | background-position: left; 172 | background-size: 22px; 173 | background-origin: content-box; 174 | } 175 | 176 | .edit-button:hover:enabled { 177 | color: #0070e0; 178 | border-color: #0070e0; 179 | filter: invert(31%) sepia(95%) saturate(3744%) hue-rotate(197deg) 180 | brightness(95%) contrast(101%); 181 | } 182 | 183 | .email-container { 184 | display: flex; 185 | flex-direction: row; 186 | align-items: center; 187 | gap: 16px; 188 | width: 100%; 189 | } 190 | 191 | .email-input-with-watermark { 192 | flex: auto; 193 | margin: 0; 194 | } 195 | 196 | .email-section { 197 | display: flex; 198 | flex-direction: column; 199 | width: 100%; 200 | } 201 | 202 | .form-row { 203 | display: flex; 204 | gap: 1rem; 205 | } 206 | 207 | .form-group { 208 | position: relative; 209 | flex: 1; 210 | } 211 | 212 | .header { 213 | align-items: center; 214 | display: flex; 215 | justify-content: space-between; 216 | margin: 16px 0; 217 | } 218 | 219 | .label { 220 | position: absolute; 221 | top: 1.25rem; 222 | right: 0.75rem; 223 | left: 0.75rem; 224 | margin-bottom: 0; 225 | 226 | pointer-events: none; 227 | color: #545d68; 228 | background-color: transparent; 229 | background-clip: padding-box; 230 | font-size: 1.125rem; 231 | line-height: 1.5rem; 232 | 233 | transition: all 0.2s; 234 | transition-timing-function: cubic-bezier(0.2, 0, 0, 1); 235 | } 236 | 237 | .summary { 238 | margin: 32px 0 16px 0; 239 | } 240 | 241 | .submit-button:not([hidden]) { 242 | background-color: #003087; 243 | border: none; 244 | color: #ffffff; 245 | display: block; 246 | font-size: 18px; 247 | height: 48px; 248 | line-height: 24px; 249 | margin: 0 auto; 250 | padding: 12px 32px 12px 32px; 251 | } 252 | 253 | .submit-button:not([hidden]):hover:enabled { 254 | background-color: #0070e0; 255 | border-color: #0070e0; 256 | } 257 | 258 | .submit-button:not(#email-submit-button) { 259 | border-radius: 1000px; 260 | margin-top: 32px; 261 | } 262 | 263 | .submit-button:disabled { 264 | background-color: #dbdde0; 265 | } 266 | 267 | #payment:not(.active, .pinned) ~ #checkout-button { 268 | display: none; 269 | } 270 | 271 | #watermark-container { 272 | align-self: flex-end; 273 | min-height: 24px; 274 | margin-right: 155px; 275 | } 276 | 277 | .input-invalid { 278 | border: 1.5px solid #e00034; 279 | outline: 0.125rem solid #f5093e; 280 | } 281 | -------------------------------------------------------------------------------- /client/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import './assets/main.css' 2 | 3 | import { createHead } from 'unhead' 4 | import { createApp } from 'vue' 5 | import App from './App.vue' 6 | import router from './router' 7 | 8 | const app = createApp(App) 9 | const head = createHead() 10 | 11 | app.use(head) 12 | app.use(router) 13 | 14 | app.mount('#app') 15 | -------------------------------------------------------------------------------- /client/vue/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | import CheckoutSwitcher from '../views/CheckoutSwitcher.vue'; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/:catchAll(.*)', 9 | name: 'checkout-switcher', 10 | component: CheckoutSwitcher, 11 | }, 12 | ], 13 | }); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /client/vue/src/utils/form.js: -------------------------------------------------------------------------------- 1 | export function validateFields(form, fields = []) { 2 | if (fields.length <= 0) return true; 3 | 4 | let valid = true; 5 | const invalidFields = []; 6 | 7 | for (let i = 0; i < fields.length; i++) { 8 | const currentFieldName = fields[i]; 9 | const currentFieldElement = form.elements[currentFieldName]; 10 | const isCurrentFieldValid = currentFieldElement.checkValidity(); 11 | 12 | if (!isCurrentFieldValid) { 13 | valid = false; 14 | invalidFields.push(currentFieldName); 15 | currentFieldElement.classList.add('input-invalid'); 16 | continue; 17 | } 18 | 19 | currentFieldElement.classList.remove('input-invalid'); 20 | } 21 | 22 | if (invalidFields.length > 0) { 23 | const [firstInvalidField] = invalidFields; 24 | form.elements[firstInvalidField].reportValidity(); 25 | } 26 | 27 | return valid; 28 | } 29 | -------------------------------------------------------------------------------- /client/vue/src/views/CheckoutSwitcher.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | const fullReloadAlways = { 7 | name: 'full-reload', 8 | handleHotUpdate({ server }) { 9 | server.ws.send({ type: 'full-reload' }); 10 | return []; 11 | }, 12 | }; 13 | // https://vitejs.dev/config/ 14 | export default defineConfig({ 15 | build: { 16 | target: 'es2022', 17 | }, 18 | plugins: [vue(), fullReloadAlways], 19 | resolve: { 20 | alias: { 21 | '@': fileURLToPath(new URL('./src', import.meta.url)), 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | # Provide a comma-separated list of the domain name(s) where Fastlane will be presented 2 | DOMAINS=paypal.com 3 | 4 | # Create an application to obtain credentials at https://developer.paypal.com/dashboard/applications/sandbox 5 | PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_HERE 6 | PAYPAL_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE 7 | 8 | # set if acting on behalf of another merchant 9 | # PAYPAL_MERCHANT_ID=PARTNER_MERCHANT_ID_HERE 10 | # PAYPAL_BN_CODE=BN_CODE_HERE 11 | -------------------------------------------------------------------------------- /server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.FileProviders; 2 | using DotNetEnv; 3 | using Microsoft.AspNetCore.StaticFiles; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | Env.Load(); 8 | 9 | // Enable CORS middlware 10 | builder.Services.AddCors(options => 11 | { 12 | options.AddDefaultPolicy( 13 | policy => 14 | { 15 | policy.WithOrigins("*").AllowAnyHeader().AllowAnyMethod(); 16 | }); 17 | }); 18 | 19 | // Add services to the container. 20 | builder.Services.Configure( 21 | builder.Configuration.GetSection("TemplateSettings")); 22 | builder.Services.AddSingleton(); 23 | builder.Services.AddControllers(); 24 | builder.Services.AddHttpClient(); 25 | 26 | 27 | var app = builder.Build(); 28 | 29 | 30 | app.UseDefaultFiles(new DefaultFilesOptions 31 | { 32 | FileProvider = new PhysicalFileProvider( 33 | Path.Combine(app.Environment.ContentRootPath, "../../client/html/src")), 34 | RequestPath = "" 35 | }); 36 | 37 | // Use static files middleware to serve static files 38 | app.UseStaticFiles(new StaticFileOptions 39 | { 40 | FileProvider = new PhysicalFileProvider( 41 | Path.Combine(app.Environment.ContentRootPath, "../../client/html/src")), 42 | RequestPath = "", 43 | }); 44 | 45 | app.UseRouting(); 46 | 47 | app.UseCors(); 48 | 49 | app.MapControllers(); 50 | 51 | // Get port from environment variables or use default and run the server 52 | var port = System.Environment.GetEnvironmentVariable("PORT") ?? "8080"; 53 | app.Run($"http://localhost:{port}"); 54 | -------------------------------------------------------------------------------- /server/dotnet/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:34197", 8 | "sslPort": 44396 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": false, 16 | "launchUrl": "", 17 | "applicationUrl": "http://localhost:5089", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": false, 26 | "launchUrl": "", 27 | "applicationUrl": "https://localhost:7119;http://localhost:5089", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": false, 35 | "launchUrl": "", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # .NET Example 2 | 3 | This folder contains example code for a Fastlane integration using .NET to complete transactions with PayPal's REST APIs. 4 | 5 | ## Prerequisites 6 | 7 | - .NET 6.0 SDK or later 8 | - .NET CLI 9 | - Visual Studio 2019 or later / Visual Studio Code 10 | - Configured `.env` file (see README in repository root) 11 | 12 | ## How to run 13 | 14 | 1. Restore the required packages 15 | ``` 16 | dotnet restore 17 | ``` 18 | 2. Build and run the application 19 | ``` 20 | dotnet run 21 | ``` 22 | 3. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. 23 | -------------------------------------------------------------------------------- /server/dotnet/TemplateResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Stubble.Core.Builders; 3 | 4 | public class TemplatePathResolver 5 | { 6 | private readonly TemplateSettings _templateSettings; 7 | private readonly StubbleBuilder _stubbleBuilder; 8 | 9 | public TemplatePathResolver(IOptions templateSettings) 10 | { 11 | _templateSettings = templateSettings.Value; 12 | _stubbleBuilder = new StubbleBuilder(); 13 | InitializeFullTemplateDirectory(); 14 | } 15 | 16 | private void InitializeFullTemplateDirectory() 17 | { 18 | if (_templateSettings.TemplateRelativePath != null) 19 | { 20 | var currentDirectory = Directory.GetCurrentDirectory(); 21 | var projectDirectory = Directory.GetParent(currentDirectory)?.FullName ?? currentDirectory; 22 | _templateSettings.FullTemplateDirectory = Path.Combine(projectDirectory, _templateSettings.TemplateRelativePath); 23 | } 24 | } 25 | 26 | public async Task RenderTemplateAsync(bool isFlexibleIntegration, Dictionary locals) 27 | { 28 | var templateName = isFlexibleIntegration ? "checkout-flexible.html" : "checkout.html"; 29 | var templatePath = Path.Combine(_templateSettings.FullTemplateDirectory, templateName); 30 | 31 | if (!File.Exists(templatePath)) 32 | { 33 | throw new FileNotFoundException($"Template file not found: {templatePath}"); 34 | } 35 | 36 | var template = await File.ReadAllTextAsync(templatePath); 37 | var renderedHtml = _stubbleBuilder.Build().Render(template, locals); 38 | return renderedHtml; 39 | } 40 | } 41 | 42 | public class TemplateSettings 43 | { 44 | public required string TemplateRelativePath { get; set; } 45 | public required string FullTemplateDirectory { get; set; } 46 | } 47 | -------------------------------------------------------------------------------- /server/dotnet/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "TemplateSettings": { 3 | "TemplateRelativePath": "shared/views" 4 | } 5 | } -------------------------------------------------------------------------------- /server/dotnet/dotnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /server/java/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip 20 | -------------------------------------------------------------------------------- /server/java/README.md: -------------------------------------------------------------------------------- 1 | # Java Example 2 | 3 | This folder contains example code for a Fastlane integration using Java to complete transactions with PayPal's REST APIs. 4 | 5 | ## Prerequisites 6 | 7 | - Java 11+ 8 | - Apache Maven 9 | - Configured `.env` file (see README in repository root) 10 | 11 | ## How to run 12 | 13 | 1. Start the server 14 | ``` 15 | mvn spring-boot:run 16 | ``` 17 | 2. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. 18 | -------------------------------------------------------------------------------- /server/java/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.1 9 | 10 | 11 | com.fastlane 12 | paypal-sample 13 | 0.0.1-SNAPSHOT 14 | war 15 | java 16 | Fastlane PayPal sample application 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 17 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-actuator 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-web 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-mustache 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-tomcat 50 | provided 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-test 55 | test 56 | 57 | 58 | 59 | io.github.cdimascio 60 | dotenv-java 61 | 3.0.0 62 | 63 | 64 | org.apache.commons 65 | commons-lang3 66 | 3.14.0 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/JavaApplication.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class JavaApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(JavaApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(JavaApplication.class); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/Address.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class Address { 4 | 5 | private String addressLine1; 6 | private String addressLine2; 7 | private String adminArea2; 8 | private String adminArea1; 9 | private String postalCode; 10 | private String countryCode; 11 | 12 | public String getAddressLine1() { 13 | return addressLine1; 14 | } 15 | 16 | public void setAddressLine1(String addressLine1) { 17 | this.addressLine1 = addressLine1; 18 | } 19 | 20 | public String getAddressLine2() { 21 | return addressLine2; 22 | } 23 | 24 | public void setAddressLine2(String addressLine2) { 25 | this.addressLine2 = addressLine2; 26 | } 27 | 28 | public String getAdminArea2() { 29 | return adminArea2; 30 | } 31 | 32 | public void setAdminArea2(String adminArea2) { 33 | this.adminArea2 = adminArea2; 34 | } 35 | 36 | public String getAdminArea1() { 37 | return adminArea1; 38 | } 39 | 40 | public void setAdminArea1(String adminArea1) { 41 | this.adminArea1 = adminArea1; 42 | } 43 | 44 | public String getPostalCode() { 45 | return postalCode; 46 | } 47 | 48 | public void setPostalCode(String postalCode) { 49 | this.postalCode = postalCode; 50 | } 51 | 52 | public String getCountryCode() { 53 | return countryCode; 54 | } 55 | 56 | public void setCountryCode(String countryCode) { 57 | this.countryCode = countryCode; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/AuthResponse.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | @JsonIgnoreProperties(ignoreUnknown = true) 7 | public class AuthResponse { 8 | 9 | @JsonProperty("access_token") 10 | private String accessToken; 11 | 12 | public String getAccessToken() { 13 | return accessToken; 14 | } 15 | 16 | public void setAccessToken(String clientToken) { 17 | this.accessToken = clientToken; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/ClientTokenResponse.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class ClientTokenResponse { 4 | 5 | private String clientToken; 6 | private String clientId; 7 | private String paypalSdkBaseUrl; 8 | 9 | public ClientTokenResponse(String clientId, String clientToken, String paypalSdkBaseUrl) { 10 | this.clientId = clientId; 11 | this.clientToken = clientToken; 12 | this.paypalSdkBaseUrl = paypalSdkBaseUrl; 13 | } 14 | 15 | public String getClientToken() { 16 | return clientToken; 17 | } 18 | 19 | public void setClientToken(String clientToken) { 20 | this.clientToken = clientToken; 21 | } 22 | 23 | public String getClientId() { 24 | return clientId; 25 | } 26 | 27 | public void setClientId(String clientId) { 28 | this.clientId = clientId; 29 | } 30 | 31 | public String getPaypalSdkBaseUrl() { 32 | return paypalSdkBaseUrl; 33 | } 34 | 35 | public void setPaypalSdkBaseUrl(String paypalSdkBaseUrl) { 36 | this.paypalSdkBaseUrl = paypalSdkBaseUrl; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/Name.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class Name { 4 | 5 | private String fullName; 6 | 7 | public String getFullName() { 8 | return fullName; 9 | } 10 | 11 | public void setFullName(String fullName) { 12 | this.fullName = fullName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/PaymentToken.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class PaymentToken { 4 | 5 | private String id; 6 | 7 | public String getId() { 8 | return id; 9 | } 10 | 11 | public void setId(String id) { 12 | this.id = id; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/PhoneNumber.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class PhoneNumber { 4 | 5 | private String countryCode; 6 | private String nationalNumber; 7 | 8 | public String getCountryCode() { 9 | return countryCode; 10 | } 11 | 12 | public void setCountryCode(String countryCode) { 13 | this.countryCode = countryCode; 14 | } 15 | 16 | public String getNationalNumber() { 17 | return nationalNumber; 18 | } 19 | 20 | public void setNationalNumber(String nationalNumber) { 21 | this.nationalNumber = nationalNumber; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/Request.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class Request { 4 | 5 | private PaymentToken paymentToken; 6 | private ShippingAddress shippingAddress; 7 | 8 | public PaymentToken getPaymentToken() { 9 | return paymentToken; 10 | } 11 | 12 | public void setPaymentToken(PaymentToken paymentToken) { 13 | this.paymentToken = paymentToken; 14 | } 15 | 16 | public ShippingAddress getShippingAddress() { 17 | return shippingAddress; 18 | } 19 | 20 | public void setShippingAddress(ShippingAddress shippingAddress) { 21 | this.shippingAddress = shippingAddress; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/fastlane/paypalsample/sample/models/ShippingAddress.java: -------------------------------------------------------------------------------- 1 | package com.fastlane.paypalsample.sample.models; 2 | 3 | public class ShippingAddress { 4 | 5 | private Address address; 6 | private PhoneNumber phoneNumber; 7 | private Name name; 8 | private String companyName; 9 | 10 | public String getCompanyName() { 11 | return companyName; 12 | } 13 | 14 | public void setCompanyName(String companyName) { 15 | this.companyName = companyName; 16 | } 17 | 18 | public Address getAddress() { 19 | return address; 20 | } 21 | 22 | public void setAddress(Address address) { 23 | this.address = address; 24 | } 25 | 26 | public PhoneNumber getPhoneNumber() { 27 | return phoneNumber; 28 | } 29 | 30 | public void setPhoneNumber(PhoneNumber phoneNumber) { 31 | this.phoneNumber = phoneNumber; 32 | } 33 | 34 | public Name getName() { 35 | return name; 36 | } 37 | 38 | public void setName(Name name) { 39 | this.name = name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/java/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=java 2 | spring.web.resources.static-locations=file:../../client/html/src/ 3 | server.servlet.encoding.charset=UTF-8 4 | server.servlet.encoding.enabled=true 5 | server.servlet.encoding.force=true 6 | spring.mustache.suffix=.html 7 | spring.mustache.prefix=file:../shared/views/ 8 | spring.mustache.charset=UTF-8 9 | spring.mustache.servlet.cache=false -------------------------------------------------------------------------------- /server/node/README.md: -------------------------------------------------------------------------------- 1 | # Node.js Example 2 | 3 | This folder contains example code for a Fastlane integration using Node.js to complete transactions with PayPal's REST APIs. 4 | 5 | ## Prerequisites 6 | 7 | - [Node.js v18+](https://nodejs.org) 8 | - [npm](https://www.npmjs.com/) 9 | - Configured `.env` file (see README in repository root) 10 | 11 | ## How to run 12 | 13 | 1. Install required packages 14 | ``` 15 | npm install 16 | ``` 17 | 2. Start the server 18 | ``` 19 | npm start 20 | ``` 21 | 3. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. 22 | -------------------------------------------------------------------------------- /server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastlane-sample-application", 3 | "version": "1.0.0", 4 | "description": "Sample Node.js web app using PayPal's REST APIs to integrate Fastlane for online payments", 5 | "main": "src/server.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/server.js" 9 | }, 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "consolidate": "^1.0.3", 13 | "cors": "^2.8.5", 14 | "dotenv": "^16.4.5", 15 | "express": "^4.19.2", 16 | "mustache": "^4.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/node/src/server.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import engines from 'consolidate'; 3 | import express from 'express'; 4 | import cors from 'cors'; 5 | 6 | const { 7 | PAYPAL_API_BASE_URL = 'https://api-m.sandbox.paypal.com', // use https://api-m.paypal.com for production environment 8 | PAYPAL_SDK_BASE_URL = 'https://www.sandbox.paypal.com', // use https://www.paypal.com for production environment 9 | PAYPAL_CLIENT_ID, 10 | PAYPAL_CLIENT_SECRET, 11 | DOMAINS, 12 | PAYPAL_MERCHANT_ID, 13 | PAYPAL_BN_CODE, 14 | } = process.env; 15 | 16 | /* ###################################################################### 17 | * Token generation helpers 18 | * ###################################################################### */ 19 | 20 | function getAuthAssertionToken(clientId, merchantId) { 21 | const header = { 22 | alg: 'none', 23 | }; 24 | const body = { 25 | iss: clientId, 26 | payer_id: merchantId, 27 | }; 28 | const signature = ''; 29 | const jwtParts = [header, body, signature]; 30 | 31 | const authAssertion = jwtParts 32 | .map((part) => part && btoa(JSON.stringify(part))) 33 | .join('.'); 34 | 35 | return authAssertion; 36 | } 37 | 38 | async function getClientToken() { 39 | try { 40 | if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { 41 | throw new Error('Missing API credentials'); 42 | } 43 | 44 | const url = `${PAYPAL_API_BASE_URL}/v1/oauth2/token`; 45 | 46 | const headers = new Headers(); 47 | 48 | const auth = Buffer.from( 49 | `${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`, 50 | ).toString('base64'); 51 | 52 | headers.append('Authorization', `Basic ${auth}`); 53 | headers.append('Content-Type', 'application/x-www-form-urlencoded'); 54 | 55 | if (PAYPAL_MERCHANT_ID) { 56 | headers.append( 57 | 'PayPal-Auth-Assertion', 58 | getAuthAssertionToken(PAYPAL_CLIENT_ID, PAYPAL_MERCHANT_ID), 59 | ); 60 | } 61 | 62 | const searchParams = new URLSearchParams(); 63 | searchParams.append('grant_type', 'client_credentials'); 64 | searchParams.append('response_type', 'client_token'); 65 | searchParams.append('intent', 'sdk_init'); 66 | searchParams.append('domains[]', DOMAINS); 67 | 68 | const options = { 69 | method: 'POST', 70 | headers, 71 | body: searchParams, 72 | }; 73 | 74 | const response = await fetch(url, options); 75 | const data = await response.json(); 76 | 77 | return data.access_token; 78 | } catch (error) { 79 | console.error(error); 80 | 81 | return ''; 82 | } 83 | } 84 | 85 | async function getAccessToken() { 86 | if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) { 87 | throw new Error('Missing API credentials'); 88 | } 89 | 90 | const url = `${PAYPAL_API_BASE_URL}/v1/oauth2/token`; 91 | 92 | const headers = new Headers(); 93 | const auth = Buffer.from( 94 | `${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`, 95 | ).toString('base64'); 96 | headers.append('Authorization', `Basic ${auth}`); 97 | headers.append('Content-Type', 'application/x-www-form-urlencoded'); 98 | if (PAYPAL_MERCHANT_ID) { 99 | headers.append('PayPal-Partner-Attribution-ID', PAYPAL_BN_CODE); 100 | headers.append( 101 | 'PayPal-Auth-Assertion', 102 | getAuthAssertionToken(PAYPAL_CLIENT_ID, PAYPAL_MERCHANT_ID), 103 | ); 104 | } 105 | 106 | const searchParams = new URLSearchParams(); 107 | searchParams.append('grant_type', 'client_credentials'); 108 | 109 | const options = { 110 | method: 'POST', 111 | headers, 112 | body: searchParams, 113 | }; 114 | 115 | const response = await fetch(url, options); 116 | const data = await response.json(); 117 | 118 | return data.access_token; 119 | } 120 | 121 | /* ###################################################################### 122 | * Serve checkout page 123 | * ###################################################################### */ 124 | 125 | function getPayPalSdkUrl() { 126 | const sdkUrl = new URL('/sdk/js', PAYPAL_SDK_BASE_URL); 127 | const sdkParams = new URLSearchParams({ 128 | 'client-id': PAYPAL_CLIENT_ID, 129 | components: 'buttons,fastlane', 130 | }); 131 | sdkUrl.search = sdkParams.toString(); 132 | 133 | return sdkUrl.toString(); 134 | } 135 | 136 | async function renderCheckout(req, res) { 137 | const isFlexibleIntegration = req.query.flexible !== undefined; 138 | 139 | const sdkUrl = getPayPalSdkUrl(); 140 | const clientToken = await getClientToken(); 141 | const locals = { 142 | title: 143 | 'Fastlane - PayPal Integration' + 144 | (isFlexibleIntegration ? ' (Flexible)' : ''), 145 | prerequisiteScripts: ` 146 | 151 | `, 152 | initScriptPath: isFlexibleIntegration 153 | ? 'init-fastlane-flexible.js' 154 | : 'init-fastlane.js', 155 | stylesheetPath: 'styles.css', 156 | }; 157 | 158 | res.render(isFlexibleIntegration ? 'checkout-flexible' : 'checkout', locals); 159 | } 160 | 161 | /* ###################################################################### 162 | * Process transactions 163 | * ###################################################################### */ 164 | 165 | async function createOrder(req, res) { 166 | try { 167 | const { paymentToken, shippingAddress } = req.body; 168 | 169 | const url = `${PAYPAL_API_BASE_URL}/v2/checkout/orders`; 170 | 171 | const headers = new Headers(); 172 | const accessToken = await getAccessToken(); 173 | headers.append('PayPal-Request-Id', Date.now().toString()); 174 | headers.append('Authorization', `Bearer ${accessToken}`); 175 | headers.append('Content-Type', 'application/json'); 176 | 177 | const { fullName } = shippingAddress?.name ?? {}; 178 | const { countryCode, nationalNumber } = shippingAddress?.phoneNumber ?? {}; 179 | const payload = { 180 | intent: 'CAPTURE', 181 | payment_source: { 182 | card: { 183 | single_use_token: paymentToken.id, 184 | }, 185 | }, 186 | purchase_units: [ 187 | { 188 | amount: { 189 | currency_code: 'USD', 190 | value: '110.00', 191 | }, 192 | ...(shippingAddress && { 193 | shipping: { 194 | type: 'SHIPPING', 195 | ...(fullName && { 196 | name: { 197 | full_name: fullName, 198 | }, 199 | }), 200 | company_name: shippingAddress.companyName || null, 201 | address: { 202 | address_line_1: shippingAddress.address.addressLine1, 203 | address_line_2: shippingAddress.address.addressLine2, 204 | admin_area_2: shippingAddress.address.adminArea2, 205 | admin_area_1: shippingAddress.address.adminArea1, 206 | postal_code: shippingAddress.address.postalCode, 207 | country_code: shippingAddress.address.countryCode, 208 | }, 209 | ...(countryCode && 210 | nationalNumber && { 211 | phone_number: { 212 | country_code: countryCode, 213 | national_number: nationalNumber, 214 | }, 215 | }), 216 | }, 217 | }), 218 | }, 219 | ], 220 | }; 221 | 222 | const response = await fetch(url, { 223 | method: 'POST', 224 | headers, 225 | body: JSON.stringify(payload), 226 | }); 227 | const result = await response.json(); 228 | 229 | res.status(response.status).json({ result }); 230 | } catch (error) { 231 | console.error(error); 232 | res.status(500).json({ error: error.message }); 233 | } 234 | } 235 | 236 | /* ###################################################################### 237 | * Run the server 238 | * ###################################################################### */ 239 | 240 | function configureServer(app) { 241 | app.engine('html', engines.mustache); 242 | app.set('view engine', 'html'); 243 | app.set('views', '../shared/views'); 244 | 245 | app.enable('strict routing'); 246 | 247 | app.use(cors()); 248 | app.use(express.json()); 249 | 250 | app.get('/', renderCheckout); 251 | app.post('/transaction', createOrder); 252 | 253 | app.get('/sdk/url', (_req, res) => { 254 | const sdkUrl = getPayPalSdkUrl(); 255 | res.json({ url: sdkUrl }); 256 | }); 257 | 258 | app.get('/sdk/client-token', async (_req, res) => { 259 | const clientToken = await getClientToken(); 260 | res.json({ clientToken }); 261 | }); 262 | 263 | app.use(express.static('../../client/html/src')); 264 | } 265 | 266 | const app = express(); 267 | 268 | configureServer(app); 269 | 270 | const port = process.env.PORT ?? 8080; 271 | 272 | app.listen(port, () => { 273 | console.log(`Fastlane Sample Application - Server listening at port ${port}`); 274 | }); 275 | -------------------------------------------------------------------------------- /server/php/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | /public/* 11 | !/public/index.php 12 | ###< symfony/framework-bundle ### 13 | -------------------------------------------------------------------------------- /server/php/README.md: -------------------------------------------------------------------------------- 1 | # PHP Example 2 | 3 | This folder contains example code for a Fastlane integration using PHP to complete transactions with PayPal's SDK. 4 | 5 | ## Prerequisites 6 | 7 | - [PHP 8.2 or later](https://www.php.net/manual/en/install.php) 8 | - [Composer](https://getcomposer.org/download/) 9 | - Configured `.env` file (see README in repository root) 10 | 11 | ## How to run 12 | 13 | 1. Install required packages 14 | ``` 15 | composer install 16 | ``` 17 | 2. Start the server 18 | ``` 19 | php -S localhost:8080 -t public/ 20 | ``` 21 | 3. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. -------------------------------------------------------------------------------- /server/php/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =8.2", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "braintree/braintree_php": "6.19.0", 11 | "mustache/mustache": "^2.14", 12 | "nelmio/cors-bundle": "^2.5", 13 | "symfony/console": "7.1.*", 14 | "symfony/dotenv": "7.1.*", 15 | "symfony/flex": "^2", 16 | "symfony/framework-bundle": "7.1.*", 17 | "symfony/http-client": "7.1.*", 18 | "symfony/http-foundation": "7.1.*", 19 | "symfony/routing": "7.1.*", 20 | "symfony/runtime": "7.1.*", 21 | "symfony/yaml": "7.1.*" 22 | }, 23 | "config": { 24 | "allow-plugins": { 25 | "php-http/discovery": true, 26 | "symfony/flex": true, 27 | "symfony/runtime": true 28 | }, 29 | "sort-packages": true 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "App\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "App\\Tests\\": "tests/" 39 | } 40 | }, 41 | "replace": { 42 | "symfony/polyfill-ctype": "*", 43 | "symfony/polyfill-iconv": "*", 44 | "symfony/polyfill-php72": "*", 45 | "symfony/polyfill-php73": "*", 46 | "symfony/polyfill-php74": "*", 47 | "symfony/polyfill-php80": "*", 48 | "symfony/polyfill-php81": "*", 49 | "symfony/polyfill-php82": "*" 50 | }, 51 | "scripts": { 52 | "auto-scripts": { 53 | "cache:clear": "symfony-cmd", 54 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 55 | }, 56 | "post-install-cmd": [ 57 | "@auto-scripts" 58 | ], 59 | "post-update-cmd": [ 60 | "@auto-scripts" 61 | ] 62 | }, 63 | "conflict": { 64 | "symfony/symfony": "*" 65 | }, 66 | "extra": { 67 | "symfony": { 68 | "allow-contrib": false, 69 | "require": "7.1.*" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/php/config/bundles.php: -------------------------------------------------------------------------------- 1 | ["all" => true], 5 | Nelmio\CorsBundle\NelmioCorsBundle::class => ["all" => true], 6 | ]; 7 | -------------------------------------------------------------------------------- /server/php/config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /server/php/config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | 6 | # Note that the session will be started ONLY if you read or write from it. 7 | session: true 8 | 9 | #esi: true 10 | #fragments: true 11 | 12 | when@test: 13 | framework: 14 | test: true 15 | session: 16 | storage_factory_id: session.storage.factory.mock_file 17 | -------------------------------------------------------------------------------- /server/php/config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ['*'] 5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 6 | allow_headers: ['Content-Type', 'Authorization'] 7 | expose_headers: ['Link'] 8 | max_age: 3600 9 | paths: 10 | '^/': null 11 | -------------------------------------------------------------------------------- /server/php/config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /server/php/config/preload.php: -------------------------------------------------------------------------------- 1 | add("getCheckout", "/") 9 | ->controller([ServerController::class, "index"]); 10 | 11 | $routes 12 | ->add("createTransaction", "/transaction") 13 | ->controller([ServerController::class, "createTransaction"]); 14 | 15 | $routes 16 | ->add("getSdkUrl", "/sdk/url") 17 | ->controller([ServerController::class, "getSDKUrl"]); 18 | 19 | $routes 20 | ->add("getClientToken", "/sdk/client-token") 21 | ->controller([ServerController::class, "getClientToken"]); 22 | }; 23 | -------------------------------------------------------------------------------- /server/php/config/routes/framework.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | _errors: 3 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 4 | prefix: /_error 5 | -------------------------------------------------------------------------------- /server/php/config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration 6 | parameters: 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true # Automatically injects dependencies in your services. 12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 13 | 14 | # makes classes in src/ available to be used as services 15 | # this creates a service per class whose id is the fully-qualified class name 16 | App\: 17 | resource: '../src/' 18 | exclude: 19 | - '../src/DependencyInjection/' 20 | - '../src/Entity/' 21 | - '../src/Kernel.php' 22 | 23 | # add more service definitions when explicit configuration is needed 24 | # please note that last definitions always *replace* previous ones 25 | -------------------------------------------------------------------------------- /server/php/public/index.php: -------------------------------------------------------------------------------- 1 | copy($sourceFile, $destFile); 30 | } 31 | } catch (\Exception $e) { 32 | echo $e->getMessage(); 33 | } 34 | } 35 | 36 | return new Kernel($context["APP_ENV"], (bool) $context["APP_DEBUG"]); 37 | }; 38 | -------------------------------------------------------------------------------- /server/php/src/Kernel.php: -------------------------------------------------------------------------------- 1 | 4.20.0' 5 | gem 'dotenv', groups: %i[development test] 6 | gem 'mustache', '~> 1.0' 7 | gem 'webrick' 8 | -------------------------------------------------------------------------------- /server/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | braintree (4.20.0) 5 | builder (>= 3.2.4) 6 | rexml (>= 3.1.9) 7 | builder (3.3.0) 8 | dotenv (2.8.1) 9 | mustache (1.1.1) 10 | rexml (3.3.2) 11 | strscan 12 | strscan (3.1.0) 13 | webrick (1.8.1) 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | braintree (~> 4.20.0) 20 | dotenv 21 | mustache (~> 1.0) 22 | webrick 23 | 24 | BUNDLED WITH 25 | 1.17.2 26 | -------------------------------------------------------------------------------- /server/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Ruby Example 2 | 3 | This folder contains example code for a Fastlane integration using Ruby to complete transactions with PayPal's SDK. 4 | 5 | ## Prerequisites 6 | 7 | - [Ruby 2.6 or later](https://www.ruby-lang.org/) 8 | - [Bundler](https://bundler.io/) 9 | - Configured `.env` file (see README in repository root) 10 | 11 | ## How to run 12 | 13 | 1. Install required packages 14 | ``` 15 | bundle install --path vendor/bundle 16 | ``` 17 | 2. Start the server 18 | ``` 19 | ruby src/server.rb 20 | ``` 21 | 3. To view the application in your browser, choose a front-end implementation from the `client` folder at the root of this repository and follow the instructions in that folder's README. 22 | -------------------------------------------------------------------------------- /server/shared/views/checkout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | 5 | 6 | 7 | 8 | {{&prerequisiteScripts}} 9 | 10 | 11 | 12 | 13 | 14 | {{title}} 15 | 16 | 17 | 18 | Customer 19 | 20 | 21 | Edit 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | E-mail 32 | 33 | 34 | Continue 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Shipping 47 | 48 | 49 | Edit 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | This purchase requires shipping 58 | 59 | 60 | 61 | 62 | 64 | First Name 65 | 66 | 67 | 69 | Last Name 70 | 71 | 72 | 73 | 74 | 76 | Company name (optional) 77 | 78 | 79 | 80 | 81 | 83 | Address Line 1 84 | 85 | 86 | 87 | 88 | 90 | 91 | Apt., ste, bldg. (optional) 92 | 93 | 94 | 95 | 96 | 97 | 99 | City 100 | 101 | 102 | 103 | 105 | State 106 | 107 | 108 | 109 | 110 | 113 | ZIP Code 114 | 115 | 116 | 118 | Country (eg. US, UK) 119 | 120 | 121 | 122 | 123 | 128 | 129 | Country calling code 130 | 131 | 132 | 133 | 134 | 138 | Phone Number 139 | 140 | 141 | 142 | 143 | Confirm 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | Payment 152 | 153 | 154 | Edit 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | Checkout 164 | 165 | 166 | 167 | 168 | --------------------------------------------------------------------------------