├── .cli.json ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── 1.Bug_report.md │ └── config.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── client ├── css │ ├── global.css │ └── normalize.css ├── favicon.ico └── index.html ├── server ├── README.md ├── dotnet │ ├── Configuration │ │ └── StripeOptions.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── global.json │ ├── server.csproj │ └── server.sln ├── go │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── server.go ├── java │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── stripe │ │ └── sample │ │ └── Server.java ├── node-typescript │ ├── README.md │ ├── package.json │ ├── src │ │ └── server.ts │ ├── tsconfig.json │ └── tslint.json ├── node │ ├── README.md │ ├── package.json │ └── server.js ├── php │ ├── .htaccess │ ├── README.md │ ├── composer.json │ └── public │ │ ├── css │ │ ├── global.css │ │ └── normalize.css │ │ ├── index.php │ │ ├── shared.php │ │ ├── utils.js │ │ └── webhook.php ├── python │ ├── README.md │ ├── requirements.txt │ └── server.py └── ruby │ ├── Gemfile │ ├── README.md │ └── server.rb └── spec ├── server_spec.rb └── spec_helper.rb /.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name-of-your-sample", 3 | "configureDotEnv": true, 4 | "integrations": [ 5 | { 6 | "name": "main", 7 | "clients": ["html"], 8 | "servers": ["java", "node", "php", "python", "ruby", "go", "dotnet", "node-typescript"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Stripe keys 2 | # https://dashboard.stripe.com/test/apikeys 3 | STRIPE_PUBLISHABLE_KEY=pk_12345 4 | STRIPE_SECRET_KEY=sk_12345 5 | # https://stripe.com/docs/webhooks/signatures 6 | STRIPE_WEBHOOK_SECRET=whsec_1234 7 | 8 | # Environment variables 9 | STATIC_DIR=../../client 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1.Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report for this sample. 4 | --- 5 | 6 | # Bug report 7 | 8 | ## Describe the bug 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | ## To Reproduce 13 | 14 | Steps to reproduce the behavior, please provide code snippets or a repository: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | ## Expected behavior 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | ## Screenshots 26 | 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | ## System information 30 | 31 | - OS: [e.g. macOS, Windows] 32 | - Browser (if applies) [e.g. chrome, safari] 33 | - Server environment [e.g. Go, Java, Node, PHP, Python, Ruby, TypeScript] 34 | 35 | ## Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Need help with your integration? 4 | url: https://webchat.freenode.net/#stripe 5 | about: Ask questions in our technical chat on IRC. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # ruby dependencies 9 | - package-ecosystem: "bundler" 10 | directory: "/server/ruby/" 11 | schedule: 12 | interval: "weekly" 13 | day: "thursday" 14 | 15 | # python dependencies 16 | - package-ecosystem: "pip" 17 | directory: "/server/python/" 18 | schedule: 19 | interval: "weekly" 20 | day: "thursday" 21 | 22 | # php dependencies 23 | - package-ecosystem: "composer" 24 | directory: "/server/php/" 25 | schedule: 26 | interval: "weekly" 27 | day: "thursday" 28 | 29 | # node dependencies 30 | - package-ecosystem: "npm" 31 | directory: "/server/node/" 32 | schedule: 33 | interval: "weekly" 34 | day: "thursday" 35 | - package-ecosystem: "npm" 36 | directory: "/server/node-typescript/" 37 | schedule: 38 | interval: "weekly" 39 | day: "thursday" 40 | 41 | # go dependencies 42 | - package-ecosystem: "gomod" 43 | directory: "/server/go/" 44 | schedule: 45 | interval: "weekly" 46 | day: "thursday" 47 | 48 | # java dependencies 49 | - package-ecosystem: "maven" 50 | directory: "/server/java/" 51 | schedule: 52 | interval: "weekly" 53 | day: "thursday" 54 | 55 | # dotnet dependencies 56 | - package-ecosystem: "nuget" 57 | directory: "/server/dotnet/" 58 | schedule: 59 | interval: "weekly" 60 | day: "thursday" 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI for stripe-samples/template 2 | on: 3 | push: 4 | branches: 5 | - '**' 6 | - '!dependabot/**' 7 | 8 | env: 9 | STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} 10 | STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} 11 | 12 | concurrency: 13 | group: ci-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | server_test: 18 | runs-on: ubuntu-20.04 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | runtime: 23 | - server_type: ruby 24 | server_image: ruby:3.0 25 | - server_type: ruby 26 | server_image: ruby:2.6 27 | - server_type: node 28 | server_image: node:14.17 29 | - server_type: node 30 | server_image: node:12.22 31 | - server_type: python 32 | server_image: python:3.9 33 | - server_type: python 34 | server_image: python:3.6 35 | - server_type: java 36 | server_image: maven:3.8-openjdk-16 37 | - server_type: java 38 | server_image: maven:3.8-openjdk-8 39 | - server_type: go 40 | server_image: golang:1.16 41 | - server_type: go 42 | server_image: golang:1.15 43 | - server_type: dotnet 44 | server_image: mcr.microsoft.com/dotnet/sdk:6.0 45 | target: 46 | - sample: custom-payment-flow 47 | tests: custom_payment_flow_server_spec.rb 48 | - sample: prebuilt-checkout-page 49 | tests: prebuilt_checkout_page_spec.rb 50 | - sample: payment-element 51 | tests: payment_element_server_spec.rb 52 | include: 53 | - runtime: 54 | server_type: node-typescript 55 | server_image: node:14.17 56 | target: 57 | sample: custom-payment-flow 58 | tests: custom_payment_flow_server_spec.rb 59 | - runtime: 60 | server_type: node-typescript 61 | server_image: node:12.22 62 | target: 63 | sample: custom-payment-flow 64 | tests: custom_payment_flow_server_spec.rb 65 | steps: 66 | - uses: actions/checkout@v2 67 | 68 | - uses: actions/checkout@v2 69 | with: 70 | repository: 'stripe-samples/sample-ci' 71 | path: 'sample-ci' 72 | 73 | - name: Setup dependencies 74 | run: | 75 | source sample-ci/helpers.sh 76 | setup_dependencies 77 | 78 | - name: Run tests 79 | run: | 80 | source sample-ci/helpers.sh 81 | 82 | install_docker_compose_settings 83 | export STRIPE_WEBHOOK_SECRET=$(retrieve_webhook_secret) 84 | cat <> .env 85 | DOMAIN=http://web:4242 86 | PRICE=${{ secrets.TEST_PRICE }} 87 | PAYMENT_METHOD_TYPES="card" 88 | EOF 89 | 90 | configure_docker_compose_for_integration "${{ matrix.target.sample }}" "${{ matrix.runtime.server_type }}" ../../client/html "${{ matrix.runtime.server_image }}" 91 | 92 | docker-compose up -d && wait_web_server 93 | docker-compose exec -T runner bundle exec rspec spec/${{ matrix.target.tests }} 94 | 95 | - name: Collect debug information 96 | if: ${{ failure() }} 97 | run: | 98 | cat .env 99 | cat docker-compose.yml 100 | docker-compose ps -a 101 | docker-compose logs web 102 | 103 | e2e_test: 104 | runs-on: ubuntu-20.04 105 | strategy: 106 | fail-fast: false 107 | matrix: 108 | implementation: 109 | - client_type: html 110 | domain: http://web:4242 111 | profile: e2e 112 | - client_type: react-cra 113 | domain: http://frontend:3000 114 | profile: frontend 115 | target: 116 | - sample: custom-payment-flow 117 | tests: custom_payment_flow_e2e_spec.rb 118 | - sample: prebuilt-checkout-page 119 | tests: prebuilt_checkout_page_e2e_spec.rb 120 | - sample: payment-element 121 | tests: payment_element_e2e_spec.rb 122 | include: 123 | - implementation: 124 | client_type: vue-cva 125 | domain: http://frontend:3000 126 | profile: frontend 127 | target: 128 | sample: prebuilt-checkout-page 129 | tests: prebuilt_checkout_page_e2e_spec.rb 130 | steps: 131 | - uses: actions/checkout@v2 132 | 133 | - uses: actions/checkout@v2 134 | with: 135 | repository: 'stripe-samples/sample-ci' 136 | path: 'sample-ci' 137 | 138 | - name: Setup dependencies 139 | run: | 140 | source sample-ci/helpers.sh 141 | setup_dependencies 142 | 143 | - name: Prepare tests 144 | run: | 145 | echo "$(cat ${{ matrix.target.sample }}/client/react-cra/package.json | jq '.proxy = "http://web:4242"')" > ${{ matrix.target.sample }}/client/react-cra/package.json 146 | 147 | - name: Run tests 148 | run: | 149 | source sample-ci/helpers.sh 150 | 151 | install_docker_compose_settings 152 | export STRIPE_WEBHOOK_SECRET=$(retrieve_webhook_secret) 153 | cat <> .env 154 | DOMAIN=${{ matrix.implementation.domain }} 155 | PRICE=${{ secrets.TEST_PRICE }} 156 | PAYMENT_METHOD_TYPES="card" 157 | EOF 158 | 159 | configure_docker_compose_for_integration "${{ matrix.target.sample }}" node ../../client/${{ matrix.implementation.client_type }} node:14.17 160 | docker-compose --profile="${{ matrix.implementation.profile }}" up -d && wait_web_server && wait_web_server "${{ matrix.implementation.domain }}" 161 | command="docker-compose exec -T runner bundle exec rspec spec/${{ matrix.target.tests }}" 162 | $command \ 163 | || $command --only-failures \ 164 | || $command --only-failures --format RSpec::Github::Formatter --format progress 165 | 166 | - name: Collect debug information 167 | if: ${{ failure() }} 168 | run: | 169 | cat .env 170 | cat docker-compose.yml 171 | docker-compose ps -a 172 | docker-compose --profile="${{ matrix.implementation.profile }}" logs web frontend 173 | 174 | docker cp $(docker-compose ps -qa runner | head -1):/work/tmp . 175 | 176 | - name: Upload capybara screenshots 177 | if: ${{ failure() }} 178 | uses: actions/upload-artifact@v2 179 | with: 180 | name: screenshots 181 | path: | 182 | tmp/capybara 183 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .vscode 4 | 5 | # Dependencies 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | !/yarn.lock 10 | composer.lock 11 | 12 | # Ruby files 13 | Gemfile.lock 14 | 15 | # Python files 16 | __pycache__ 17 | venv 18 | env 19 | 20 | # PHP files 21 | vendor 22 | logs 23 | 24 | # Java files 25 | .settings 26 | target/ 27 | .classpath 28 | .factorypath 29 | .project 30 | 31 | # Typescript 32 | dist 33 | 34 | # Dotnet files 35 | obj/ 36 | bin/ 37 | **/obj/** 38 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at conduct@stripe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this sample 2 | 3 | Thanks for contributing to this sample! 4 | 5 | ## Issues 6 | 7 | This sample shows you how to integrate with [a specific Stripe feature (link here)](). Please only file issues here that you believe represent bugs with this particular sample, not [the feature itself (link here)]() itself. 8 | 9 | If you're having general trouble with a Stripe feature, please reach out to us using the form at , or come chat with us on the [Stripe Discord server](https://discord.com/invite/rujnsbxrqn). We're very proud of our level of service, and we're more than happy to help you out with your integration. 10 | 11 | If you've found a bug in any of the implementations in this repository, please [let us know (add link to sample's new issue page)]()! 12 | 13 | ## Code review 14 | 15 | All pull requests will be reviewed by someone from Stripe before merging. At 16 | Stripe, we believe that code review is for explaining and having a discussion 17 | around code. For those new to code review, we strongly recommend [this 18 | video](https://www.youtube.com/watch?v=pjjmw9trb7s) on "code review culture." 19 | 20 | ## Developing 21 | 22 | This sample includes 8 server implementations so, to run locally, follow the steps indicated in the [main README.md](README.md#how-to-run-locally), followed with the instructions in the README of the server implemtation of your choice. 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem 'rspec' 8 | gem 'rest-client' 9 | gem 'byebug' 10 | gem 'stripe' 11 | gem 'dotenv' 12 | 13 | gem 'selenium-webdriver' 14 | gem 'capybara' 15 | gem 'capybara-screenshot' 16 | 17 | gem 'rspec-github', require: false 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stripe Sample Blueprint 2 | 3 | This is a repo to help you get started with creating a sample. 4 | 5 | 1. Clone this repository and add the sample specific logic. 6 | 7 | ``` 8 | git clone https://github.com/stripe-samples/template 9 | ``` 10 | 11 | 2. Language specific instructions: 12 | 13 | - Update the Java artifactId to use a specific sample related name. Update the README with the right package name. 14 | 15 | 3. Update the sample README below and delete this boilerplate text. 16 | 17 | 4. Update the .cli.json with details on your sample. 18 | 19 | 5. Update the sample CONTRIBUTING.md file with the correct links and Stripe features. 20 | 21 | Below is everything you should include in your original sample README. Everything above should be deleted. 22 | 23 | # Name of sample 24 | 25 | A brief description of what this sample shows. Keep it 3 - 5 sentences. 26 | 27 | A quick screenshot of the demo view: 28 | Preview of sample 29 | 30 | Features: 31 | 32 | - One cool thing about this sample 😃 33 | - Another cool thing about the sample 🏋️ 34 | - The final cool thing about the sample 💡 35 | 36 | ## How to run locally 37 | 38 | This sample includes 5 server implementations in Node, Ruby, Python, Java, and PHP. 39 | 40 | Follow the steps below to run locally. 41 | 42 | **1. Clone and configure the sample** 43 | 44 | The Stripe CLI is the fastest way to clone and configure a sample to run locally. 45 | 46 | **Using the Stripe CLI** 47 | 48 | If you haven't already installed the CLI, follow the [installation steps](https://github.com/stripe/stripe-cli#installation) in the project README. The CLI is useful for cloning samples and locally testing webhooks and Stripe integrations. 49 | 50 | In your terminal shell, run the Stripe CLI command to clone the sample: 51 | 52 | ``` 53 | stripe samples create REPLACE-WITH-NAME 54 | ``` 55 | 56 | The CLI will walk you through picking your integration type, server and client languages, and configuring your .env config file with your Stripe API keys. 57 | 58 | **Installing and cloning manually** 59 | 60 | If you do not want to use the Stripe CLI, you can manually clone and configure the sample yourself: 61 | 62 | ``` 63 | git clone https://github.com/stripe-samples/REPLACE-WITH-NAME 64 | ``` 65 | 66 | Copy the .env.example file into a file named .env in the folder of the server you want to use. For example: 67 | 68 | ``` 69 | cp .env.example server/node/.env 70 | ``` 71 | 72 | You will need a Stripe account in order to run the demo. Once you set up your account, go to the Stripe [developer dashboard](https://stripe.com/docs/development#api-keys) to find your API keys. 73 | 74 | ``` 75 | STRIPE_PUBLISHABLE_KEY= 76 | STRIPE_SECRET_KEY= 77 | ``` 78 | 79 | `STATIC_DIR` tells the server where to the client files are located and does not need to be modified unless you move the server files. 80 | 81 | **2. Follow the server instructions on how to run:** 82 | 83 | Pick the server language you want and follow the instructions in the server folder README on how to run. 84 | 85 | For example, if you want to run the Node server: 86 | 87 | ``` 88 | cd server/node # there's a README in this folder with instructions 89 | npm install 90 | npm start 91 | ``` 92 | 93 | **3. [Optional] Run a webhook locally:** 94 | 95 | If you want to test the `using-webhooks` integration with a local webhook on your machine, you can use the Stripe CLI to easily spin one up. 96 | 97 | Make sure to [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account). 98 | 99 | ``` 100 | stripe listen --forward-to localhost:4242/webhook 101 | ``` 102 | 103 | The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your .env file. 104 | 105 | You should see events logged in the console where the CLI is running. 106 | 107 | When you are ready to create a live webhook endpoint, follow our guide in the docs on [configuring a webhook endpoint in the dashboard](https://stripe.com/docs/webhooks/setup#configure-webhook-settings). 108 | 109 | **4. [Mobile clients] Set up the client app:** 110 | 111 | Finally, choose a mobile client implementation and follow the instruction in the app's README (e.g. `using-webhooks/client/ios/README.md`) to run. 112 | 113 | When the app is running, use `4242424242424242` as a test card number with any CVC code + a future expiration date. 114 | 115 | Use the `4000000000003220` test card number to trigger a 3D Secure challenge flow. 116 | 117 | Read more about testing on Stripe at https://stripe.com/docs/testing. 118 | 119 | ## FAQ 120 | 121 | Q: Why did you pick these frameworks? 122 | 123 | A: We chose the most minimal framework to convey the key Stripe calls and concepts you need to understand. These demos are meant as an educational tool that helps you roadmap how to integrate Stripe within your own system independent of the framework. 124 | 125 | ## Get support 126 | 127 | If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../issues). 128 | 129 | If you have questions, comments, or need help with code, we're here to help: 130 | 131 | - on [Discord](https://stripe.com/go/developer-chat) 132 | - on Twitter at [@StripeDev](https://twitter.com/StripeDev) 133 | - on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag 134 | 135 | Sign up to [stay updated with developer news](https://go.stripe.global/dev-digest). 136 | 137 | ## Author(s) 138 | 139 | [@adreyfus-stripe](https://twitter.com/adrind) 140 | 141 | ## Contributing 142 | 143 | If you'd like to contribute to this sample, please check out the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md) 144 | 145 | ## Code of conduct 146 | 147 | This repository has a [code of conduct](CODE_OF_CONDUCT.md), please read it before opening an issue or a PR. 148 | -------------------------------------------------------------------------------- /client/css/global.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --gray-offset: rgba(0, 0, 0, 0.03); 4 | --gray-border: rgba(0, 0, 0, 0.15); 5 | --gray-light: rgba(0, 0, 0, 0.4); 6 | --gray-mid: rgba(0, 0, 0, 0.7); 7 | --gray-dark: rgba(0, 0, 0, 0.9); 8 | --body-color: var(--gray-mid); 9 | --headline-color: var(--gray-dark); 10 | --accent-color: #0066f0; 11 | --body-font-family: -apple-system, BlinkMacSystemFont, sans-serif; 12 | --radius: 6px; 13 | --form-width: 343px; 14 | } 15 | 16 | /* Base */ 17 | * { 18 | box-sizing: border-box; 19 | } 20 | body { 21 | font-family: var(--body-font-family); 22 | font-size: 16px; 23 | color: var(--body-color); 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6 { 32 | color: var(--body-color); 33 | margin-top: 2px; 34 | margin-bottom: 4px; 35 | } 36 | h1 { 37 | font-size: 27px; 38 | color: var(--headline-color); 39 | } 40 | h4 { 41 | font-weight: 500; 42 | font-size: 14px; 43 | color: var(--gray-light); 44 | } 45 | 46 | /* Layout */ 47 | .sr-root { 48 | display: flex; 49 | flex-direction: row; 50 | width: 100%; 51 | max-width: 980px; 52 | padding: 48px; 53 | align-content: center; 54 | justify-content: center; 55 | height: auto; 56 | min-height: 100vh; 57 | margin: 0 auto; 58 | } 59 | .sr-header { 60 | margin-bottom: 32px; 61 | } 62 | .sr-payment-summary { 63 | margin-bottom: 20px; 64 | } 65 | .sr-main, 66 | .sr-content { 67 | display: flex; 68 | flex-direction: column; 69 | justify-content: center; 70 | height: 100%; 71 | align-self: center; 72 | } 73 | .sr-main { 74 | width: var(--form-width); 75 | } 76 | .sr-content { 77 | padding-left: 48px; 78 | } 79 | .sr-header__logo { 80 | background-image: var(--logo-image); 81 | height: 24px; 82 | background-size: contain; 83 | background-repeat: no-repeat; 84 | width: 100%; 85 | } 86 | .sr-legal-text { 87 | color: var(--gray-light); 88 | text-align: center; 89 | font-size: 13px; 90 | line-height: 17px; 91 | margin-top: 12px; 92 | } 93 | .sr-field-error { 94 | color: var(--accent-color); 95 | text-align: left; 96 | font-size: 13px; 97 | line-height: 17px; 98 | margin-top: 12px; 99 | } 100 | 101 | /* Form */ 102 | .sr-form-row { 103 | margin: 16px 0; 104 | } 105 | label { 106 | font-size: 13px; 107 | font-weight: 500; 108 | margin-bottom: 8px; 109 | display: inline-block; 110 | } 111 | 112 | /* Inputs */ 113 | .sr-input, 114 | .sr-select, 115 | input[type="text"] { 116 | border: 1px solid var(--gray-border); 117 | border-radius: var(--radius); 118 | padding: 5px 12px; 119 | height: 44px; 120 | width: 100%; 121 | transition: box-shadow 0.2s ease; 122 | background: white; 123 | -moz-appearance: none; 124 | -webkit-appearance: none; 125 | appearance: none; 126 | color: #32325d; 127 | } 128 | .sr-input:focus, 129 | input[type="text"]:focus, 130 | button:focus, 131 | .focused { 132 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 133 | 0 0 0 4px rgba(50, 151, 211, 0.3); 134 | outline: none; 135 | z-index: 9; 136 | } 137 | .sr-input::placeholder, 138 | input[type="text"]::placeholder { 139 | color: var(--gray-light); 140 | } 141 | 142 | /* Checkbox */ 143 | .sr-checkbox-label { 144 | position: relative; 145 | cursor: pointer; 146 | } 147 | 148 | .sr-checkbox-label input { 149 | opacity: 0; 150 | margin-right: 6px; 151 | } 152 | 153 | .sr-checkbox-label .sr-checkbox-check { 154 | position: absolute; 155 | left: 0; 156 | height: 16px; 157 | width: 16px; 158 | background-color: white; 159 | border: 1px solid var(--gray-border); 160 | border-radius: 4px; 161 | transition: all 0.2s ease; 162 | } 163 | 164 | .sr-checkbox-label input:focus ~ .sr-checkbox-check { 165 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 166 | 0 0 0 4px rgba(50, 151, 211, 0.3); 167 | outline: none; 168 | } 169 | 170 | .sr-checkbox-label input:checked ~ .sr-checkbox-check { 171 | background-color: var(--accent-color); 172 | background-repeat: no-repeat; 173 | background-size: 16px; 174 | background-position: -1px -1px; 175 | } 176 | 177 | /* Select */ 178 | .sr-select { 179 | display: block; 180 | height: 44px; 181 | margin: 0; 182 | background-repeat: no-repeat, repeat; 183 | background-position: right 12px top 50%, 0 0; 184 | background-size: 0.65em auto, 100%; 185 | } 186 | .sr-select::-ms-expand { 187 | display: none; 188 | } 189 | .sr-select:hover { 190 | cursor: pointer; 191 | } 192 | .sr-select:focus { 193 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 194 | 0 0 0 4px rgba(50, 151, 211, 0.3); 195 | outline: none; 196 | } 197 | .sr-select option { 198 | font-weight: 400; 199 | } 200 | .sr-select:invalid { 201 | color: var(--gray-light); 202 | } 203 | 204 | /* Combo inputs */ 205 | .sr-combo-inputs { 206 | display: flex; 207 | flex-direction: column; 208 | } 209 | .sr-combo-inputs input, 210 | .sr-combo-inputs .sr-select { 211 | border-radius: 0; 212 | border-bottom: 0; 213 | } 214 | .sr-combo-inputs > input:first-child, 215 | .sr-combo-inputs > .sr-select:first-child { 216 | border-radius: var(--radius) var(--radius) 0 0; 217 | } 218 | .sr-combo-inputs > input:last-child, 219 | .sr-combo-inputs > .sr-select:last-child { 220 | border-radius: 0 0 var(--radius) var(--radius); 221 | border-bottom: 1px solid var(--gray-border); 222 | } 223 | .sr-combo-inputs > .sr-combo-inputs-row:last-child input:first-child { 224 | border-radius: 0 0 0 var(--radius); 225 | border-bottom: 1px solid var(--gray-border); 226 | } 227 | .sr-combo-inputs > .sr-combo-inputs-row:last-child input:last-child { 228 | border-radius: 0 0 var(--radius) 0; 229 | border-bottom: 1px solid var(--gray-border); 230 | } 231 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:first-child { 232 | border-radius: var(--radius) 0 0 0; 233 | } 234 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:last-child { 235 | border-radius: 0 var(--radius) 0 0; 236 | } 237 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:only-child { 238 | border-radius: var(--radius) var(--radius) 0 0; 239 | } 240 | .sr-combo-inputs-row { 241 | width: 100%; 242 | display: flex; 243 | } 244 | 245 | .sr-combo-inputs-row > input { 246 | width: 100%; 247 | border-radius: 0; 248 | } 249 | 250 | .sr-combo-inputs-row > input:first-child:not(:only-child) { 251 | border-right: 0; 252 | } 253 | 254 | .sr-combo-inputs-row:not(:first-of-type) .sr-input { 255 | border-radius: 0 0 var(--radius) var(--radius); 256 | } 257 | 258 | .sr-result { 259 | height: 44px; 260 | -webkit-transition: height 1s ease; 261 | -moz-transition: height 1s ease; 262 | -o-transition: height 1s ease; 263 | transition: height 1s ease; 264 | color: var(--font-color); 265 | overflow: auto; 266 | } 267 | .sr-result code { 268 | overflow: scroll; 269 | } 270 | .sr-result.expand { 271 | height: 350px; 272 | } 273 | 274 | /* Buttons and links */ 275 | button { 276 | background: var(--accent-color); 277 | border-radius: var(--radius); 278 | color: white; 279 | border: 0; 280 | padding: 12px 16px; 281 | margin-top: 16px; 282 | font-weight: 600; 283 | cursor: pointer; 284 | transition: all 0.2s ease; 285 | display: block; 286 | } 287 | button:hover { 288 | filter: contrast(115%); 289 | } 290 | button:active { 291 | transform: translateY(0px) scale(0.98); 292 | filter: brightness(0.9); 293 | } 294 | button:disabled { 295 | opacity: 0.5; 296 | cursor: not-allowed; 297 | } 298 | 299 | .sr-payment-form button, 300 | .fullwidth { 301 | width: 100%; 302 | } 303 | 304 | a { 305 | color: var(--accent-color); 306 | text-decoration: none; 307 | transition: all 0.2s ease; 308 | } 309 | 310 | a:hover { 311 | filter: brightness(0.8); 312 | } 313 | 314 | a:active { 315 | filter: brightness(0.5); 316 | } 317 | 318 | /* Code block */ 319 | .sr-callout { 320 | background: var(--gray-offset); 321 | padding: 12px; 322 | border-radius: var(--radius); 323 | max-height: 200px; 324 | overflow: auto; 325 | } 326 | code, 327 | pre { 328 | font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace; 329 | font-size: 12px; 330 | } 331 | 332 | /* Stripe Element placeholder */ 333 | .sr-card-element { 334 | padding-top: 12px; 335 | } 336 | 337 | /* Responsiveness */ 338 | @media (max-width: 720px) { 339 | .sr-root { 340 | flex-direction: column; 341 | justify-content: flex-start; 342 | padding: 48px 20px; 343 | min-width: 320px; 344 | } 345 | 346 | .sr-header__logo { 347 | background-position: center; 348 | } 349 | 350 | .sr-payment-summary { 351 | text-align: center; 352 | } 353 | 354 | .sr-content { 355 | display: none; 356 | } 357 | 358 | .sr-main { 359 | width: 100%; 360 | } 361 | } 362 | 363 | /* Pasha styles – Brand-overrides, can split these out */ 364 | :root { 365 | --accent-color: #ed5f74; 366 | --headline-color: var(--accent-color); 367 | } 368 | 369 | .pasha-image-stack { 370 | display: grid; 371 | grid-gap: 12px; 372 | grid-template-columns: auto auto; 373 | } 374 | 375 | .pasha-image-stack img { 376 | border-radius: var(--radius); 377 | background-color: var(--gray-border); 378 | box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 379 | 0 3px 6px 0 rgba(0, 0, 0, 0.07); 380 | transition: all 0.8s ease; 381 | opacity: 0; 382 | } 383 | 384 | .pasha-image-stack img:nth-child(1) { 385 | transform: translate(12px, -12px); 386 | opacity: 1; 387 | } 388 | .pasha-image-stack img:nth-child(2) { 389 | transform: translate(-24px, 16px); 390 | opacity: 1; 391 | } 392 | .pasha-image-stack img:nth-child(3) { 393 | transform: translate(68px, -100px); 394 | opacity: 1; 395 | } 396 | 397 | /* todo: spinner/processing state, errors, animations */ 398 | 399 | .spinner, 400 | .spinner:before, 401 | .spinner:after { 402 | border-radius: 50%; 403 | } 404 | .spinner { 405 | color: #ffffff; 406 | font-size: 22px; 407 | text-indent: -99999px; 408 | margin: 0px auto; 409 | position: relative; 410 | width: 20px; 411 | height: 20px; 412 | box-shadow: inset 0 0 0 2px; 413 | -webkit-transform: translateZ(0); 414 | -ms-transform: translateZ(0); 415 | transform: translateZ(0); 416 | } 417 | .spinner:before, 418 | .spinner:after { 419 | position: absolute; 420 | content: ""; 421 | } 422 | .spinner:before { 423 | width: 10.4px; 424 | height: 20.4px; 425 | background: var(--accent-color); 426 | border-radius: 20.4px 0 0 20.4px; 427 | top: -0.2px; 428 | left: -0.2px; 429 | -webkit-transform-origin: 10.4px 10.2px; 430 | transform-origin: 10.4px 10.2px; 431 | -webkit-animation: loading 2s infinite ease 1.5s; 432 | animation: loading 2s infinite ease 1.5s; 433 | } 434 | .spinner:after { 435 | width: 10.4px; 436 | height: 10.2px; 437 | background: var(--accent-color); 438 | border-radius: 0 10.2px 10.2px 0; 439 | top: -0.1px; 440 | left: 10.2px; 441 | -webkit-transform-origin: 0px 10.2px; 442 | transform-origin: 0px 10.2px; 443 | -webkit-animation: loading 2s infinite ease; 444 | animation: loading 2s infinite ease; 445 | } 446 | @-webkit-keyframes loading { 447 | 0% { 448 | -webkit-transform: rotate(0deg); 449 | transform: rotate(0deg); 450 | } 451 | 100% { 452 | -webkit-transform: rotate(360deg); 453 | transform: rotate(360deg); 454 | } 455 | } 456 | @keyframes loading { 457 | 0% { 458 | -webkit-transform: rotate(0deg); 459 | transform: rotate(0deg); 460 | } 461 | 100% { 462 | -webkit-transform: rotate(360deg); 463 | transform: rotate(360deg); 464 | } 465 | } 466 | 467 | /* Animated form */ 468 | 469 | .sr-root { 470 | animation: 0.4s form-in; 471 | animation-fill-mode: both; 472 | animation-timing-function: ease; 473 | } 474 | 475 | .sr-payment-form .sr-form-row { 476 | animation: 0.4s field-in; 477 | animation-fill-mode: both; 478 | animation-timing-function: ease; 479 | transform-origin: 50% 0%; 480 | } 481 | 482 | /* need saas for loop :D */ 483 | .sr-payment-form .sr-form-row:nth-child(1) { 484 | animation-delay: 0; 485 | } 486 | .sr-payment-form .sr-form-row:nth-child(2) { 487 | animation-delay: 60ms; 488 | } 489 | .sr-payment-form .sr-form-row:nth-child(3) { 490 | animation-delay: 120ms; 491 | } 492 | .sr-payment-form .sr-form-row:nth-child(4) { 493 | animation-delay: 180ms; 494 | } 495 | .sr-payment-form .sr-form-row:nth-child(5) { 496 | animation-delay: 240ms; 497 | } 498 | .sr-payment-form .sr-form-row:nth-child(6) { 499 | animation-delay: 300ms; 500 | } 501 | .hidden { 502 | display: none; 503 | } 504 | 505 | @keyframes field-in { 506 | 0% { 507 | opacity: 0; 508 | transform: translateY(8px) scale(0.95); 509 | } 510 | 100% { 511 | opacity: 1; 512 | transform: translateY(0px) scale(1); 513 | } 514 | } 515 | 516 | @keyframes form-in { 517 | 0% { 518 | opacity: 0; 519 | transform: scale(0.98); 520 | } 521 | 100% { 522 | opacity: 1; 523 | transform: scale(1); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /client/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-samples/starter/3e90acfa415e06160264ccbc697f8d682118e37f/client/favicon.ico -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stripe Sample 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Sample

18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Running the server 2 | 3 | We included several RESTful server that each implement the same endpoints and 4 | logic. Pick the language you are most comfortable in and follow the 5 | instructions in the directory on how to run. 6 | 7 | # Supported languages 8 | 9 | * [JavaScript (Node)](node/README.md) 10 | * [Python (Flask)](python/README.md) 11 | * [Ruby (Sinatra)](ruby/README.md) 12 | * [PHP (Slim)](php/README.md) 13 | * [Java (Spark)](java/README.md) 14 | * [Go](go/README.md) 15 | * [.NET](dotnet/README.md) 16 | * [TypeScript (Node)](node-typescript/README.md) 17 | -------------------------------------------------------------------------------- /server/dotnet/Configuration/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | public class StripeOptions 4 | { 5 | public string PublishableKey { get; set; } 6 | public string SecretKey { get; set; } 7 | public string WebhookSecret { get; set; } 8 | public string Price { get; set; } 9 | public string Domain { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.StaticFiles.Infrastructure; 2 | using Microsoft.Extensions.FileProviders; 3 | using Microsoft.Extensions.Options; 4 | using Stripe; 5 | using Stripe.Checkout; 6 | 7 | DotNetEnv.Env.Load(); 8 | 9 | var builder = WebApplication.CreateBuilder(args); 10 | builder.Configuration.AddEnvironmentVariables(); 11 | builder.Services.Configure(options => 12 | { 13 | options.PublishableKey = builder.Configuration["STRIPE_PUBLISHABLE_KEY"]; 14 | options.SecretKey = builder.Configuration["STRIPE_SECRET_KEY"]; 15 | options.WebhookSecret = builder.Configuration["STRIPE_WEBHOOK_SECRET"]; 16 | options.Domain = builder.Configuration["DOMAIN"]; 17 | }); 18 | builder.Services.AddSingleton(new StripeClient(builder.Configuration["STRIPE_SECRET_KEY"])); 19 | 20 | var app = builder.Build(); 21 | 22 | StripeConfiguration.AppInfo = new AppInfo 23 | { 24 | Name = "stripe-samples//", 25 | Url = "https://github.com/stripe-samples", 26 | Version = "0.0.1", 27 | }; 28 | 29 | 30 | // Check any required non key .env values. 31 | // var price = Environment.GetEnvironmentVariable("PRICE"); 32 | // if (price == "price_12345" || price == "" || price == null) 33 | // { 34 | // app.Logger.LogError("You must set a Price ID in .env. Please see the README."); 35 | // Environment.Exit(1); 36 | // } 37 | 38 | if (app.Environment.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | } 42 | 43 | var staticFileOptions = new SharedOptions 44 | { 45 | FileProvider = new PhysicalFileProvider( 46 | Path.Combine(Directory.GetCurrentDirectory(), builder.Configuration["STATIC_DIR"]) 47 | ) 48 | }; 49 | app.UseDefaultFiles(new DefaultFilesOptions(staticFileOptions)); 50 | app.UseStaticFiles(new StaticFileOptions(staticFileOptions)); 51 | 52 | app.MapGet("config", async (string sessionId, IOptions options) => 53 | { 54 | return Results.Ok(new { publishableKey = options.Value.PublishableKey }); 55 | }); 56 | 57 | app.MapPost("webhook", async (HttpRequest req, IOptions options, ILogger logger) => 58 | { 59 | var json = await new StreamReader(req.Body).ReadToEndAsync(); 60 | Event stripeEvent; 61 | try 62 | { 63 | stripeEvent = EventUtility.ConstructEvent( 64 | json, 65 | req.Headers["Stripe-Signature"], 66 | options.Value.WebhookSecret 67 | ); 68 | logger.LogInformation($"Webhook notification with type: {stripeEvent.Type} found for {stripeEvent.Id}"); 69 | } 70 | catch (Exception e) 71 | { 72 | logger.LogError(e, $"Something failed => {e.Message}"); 73 | return Results.BadRequest(); 74 | } 75 | 76 | if (stripeEvent.Type == Events.CheckoutSessionCompleted) 77 | { 78 | var session = stripeEvent.Data.Object as Stripe.Checkout.Session; 79 | logger.LogInformation($"Session ID: {session.Id}"); 80 | // Take some action based on session. 81 | // Note: If you need access to the line items, for instance to 82 | // automate fullfillment based on the the ID of the Price, you'll 83 | // need to refetch the Checkout Session here, and expand the line items: 84 | // 85 | //var options = new SessionGetOptions(); 86 | // options.AddExpand("line_items"); 87 | // 88 | // var service = new SessionService(); 89 | // Session session = service.Get(session.Id, options); 90 | // 91 | // StripeList lineItems = session.LineItems; 92 | // 93 | // Read more about expand here: https://stripe.com/docs/expand 94 | } 95 | 96 | return Results.Ok(); 97 | }); 98 | 99 | app.Run(); 100 | -------------------------------------------------------------------------------- /server/dotnet/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:4242" 7 | } 8 | }, 9 | "profiles": { 10 | "IIS Express": { 11 | "commandName": "IISExpress", 12 | "launchBrowser": true, 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | }, 17 | "server": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:4242", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | An [.NET Core](https://dotnet.microsoft.com/download/dotnet-core) implementation 4 | 5 | ## Requirements 6 | 7 | * .NET Core 8 | * [Configured .env file](../../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Confirm `.env` configuration 13 | 14 | Feel free to copy `.env.example` from the root and fill in the missing values. 15 | 16 | 17 | 2. Run the application 18 | 19 | ```bash 20 | dotnet run 21 | ``` 22 | 23 | 4. If you're using the html client, go to `localhost:4242` to see the demo. For 24 | react, visit `localhost:3000`. 25 | -------------------------------------------------------------------------------- /server/dotnet/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/dotnet/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /server/dotnet/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.0", 4 | "rollForward": "minor" 5 | } 6 | } -------------------------------------------------------------------------------- /server/dotnet/server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /server/dotnet/server.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server.csproj", "{A5BE002D-4BA4-4611-B86E-87602A060827}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {A5BE002D-4BA4-4611-B86E-87602A060827}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {A5BE002D-4BA4-4611-B86E-87602A060827}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {A5BE002D-4BA4-4611-B86E-87602A060827}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {A5BE002D-4BA4-4611-B86E-87602A060827}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /server/go/README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ## How to run 4 | 5 | 1. Confirm `.env` configuration 6 | 7 | The `.env` file should be in the server directory (the one with `server.go`) 8 | 9 | If no `.env` file is found, copy the `.env.example` from the root to the server directory 10 | and update keys as shown below. 11 | 12 | Ensure the API keys are configured in `.env` in this directory. It should 13 | include the following keys: 14 | 15 | ```yaml 16 | # Stripe API keys - see https://stripe.com/docs/development/quickstart#api-keys 17 | STRIPE_PUBLISHABLE_KEY=pk_test... 18 | STRIPE_SECRET_KEY=sk_test... 19 | # Required to verify signatures in the webhook handler. 20 | # See README on how to use the Stripe CLI to test webhooks 21 | STRIPE_WEBHOOK_SECRET=whsec_... 22 | 23 | DOMAIN=http://localhost:4242 24 | 25 | # Path to front-end implementation. Note: PHP has it's own front end implementation. 26 | STATIC_DIR=../../client/html 27 | ``` 28 | 29 | 2. Install dependencies 30 | 31 | From the server directory (the one with `server.go`) run: 32 | 33 | ```sh 34 | go mod tidy 35 | go mod vendor 36 | ``` 37 | 38 | 3. Run the application 39 | 40 | Again from the server directory run: 41 | 42 | ```sh 43 | go run server.go 44 | ``` 45 | 46 | View in browser: [localhost:4242](http://localhost:4242) 47 | -------------------------------------------------------------------------------- /server/go/go.mod: -------------------------------------------------------------------------------- 1 | module stripe-sample 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/stripe/stripe-go/v72 v72.122.0 8 | ) 9 | -------------------------------------------------------------------------------- /server/go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 4 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 9 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 10 | github.com/stripe/stripe-go/v72 v72.122.0 h1:eRXWqnEwGny6dneQ5BsxGzUCED5n180u8n665JHlut8= 11 | github.com/stripe/stripe-go/v72 v72.122.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 14 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 18 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 22 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 23 | -------------------------------------------------------------------------------- /server/go/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | 13 | "github.com/joho/godotenv" 14 | "github.com/stripe/stripe-go/v72" 15 | "github.com/stripe/stripe-go/v72/webhook" 16 | ) 17 | 18 | func main() { 19 | err := godotenv.Load() 20 | if err != nil { 21 | log.Fatal("Error loading .env file") 22 | } 23 | 24 | stripe.Key = os.Getenv("STRIPE_SECRET_KEY") 25 | 26 | // For sample support and debugging, not required for production: 27 | stripe.SetAppInfo(&stripe.AppInfo{ 28 | Name: "stripe-samples/your-sample-name", 29 | Version: "0.0.1", 30 | URL: "https://github.com/stripe-samples", 31 | }) 32 | 33 | http.Handle("/", http.FileServer(http.Dir(os.Getenv("STATIC_DIR")))) 34 | http.HandleFunc("/config", handleConfig) 35 | http.HandleFunc("/webhook", handleWebhook) 36 | 37 | log.Println("server running at 0.0.0.0:4242") 38 | http.ListenAndServe("0.0.0.0:4242", nil) 39 | } 40 | 41 | // ErrorResponseMessage represents the structure of the error 42 | // object sent in failed responses. 43 | type ErrorResponseMessage struct { 44 | Message string `json:"message"` 45 | } 46 | 47 | // ErrorResponse represents the structure of the error object sent 48 | // in failed responses. 49 | type ErrorResponse struct { 50 | Error *ErrorResponseMessage `json:"error"` 51 | } 52 | 53 | func handleConfig(w http.ResponseWriter, r *http.Request) { 54 | if r.Method != "GET" { 55 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 56 | return 57 | } 58 | writeJSON(w, struct { 59 | PublishableKey string `json:"publishableKey"` 60 | }{ 61 | PublishableKey: os.Getenv("STRIPE_PUBLISHABLE_KEY"), 62 | }) 63 | } 64 | 65 | func handleWebhook(w http.ResponseWriter, r *http.Request) { 66 | if r.Method != "POST" { 67 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 68 | return 69 | } 70 | b, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | http.Error(w, err.Error(), http.StatusBadRequest) 73 | log.Printf("ioutil.ReadAll: %v", err) 74 | return 75 | } 76 | 77 | event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), os.Getenv("STRIPE_WEBHOOK_SECRET")) 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusBadRequest) 80 | log.Printf("webhook.ConstructEvent: %v", err) 81 | return 82 | } 83 | 84 | if event.Type == "checkout.session.completed" { 85 | fmt.Println("Checkout Session completed!") 86 | } 87 | 88 | writeJSON(w, nil) 89 | } 90 | 91 | func writeJSON(w http.ResponseWriter, v interface{}) { 92 | var buf bytes.Buffer 93 | if err := json.NewEncoder(&buf).Encode(v); err != nil { 94 | http.Error(w, err.Error(), http.StatusInternalServerError) 95 | log.Printf("json.NewEncoder.Encode: %v", err) 96 | return 97 | } 98 | w.Header().Set("Content-Type", "application/json") 99 | if _, err := io.Copy(w, &buf); err != nil { 100 | log.Printf("io.Copy: %v", err) 101 | return 102 | } 103 | } 104 | 105 | func writeJSONError(w http.ResponseWriter, v interface{}, code int) { 106 | w.WriteHeader(code) 107 | writeJSON(w, v) 108 | return 109 | } 110 | 111 | func writeJSONErrorMessage(w http.ResponseWriter, message string, code int) { 112 | resp := &ErrorResponse{ 113 | Error: &ErrorResponseMessage{ 114 | Message: message, 115 | }, 116 | } 117 | writeJSONError(w, resp, code) 118 | } 119 | -------------------------------------------------------------------------------- /server/java/README.md: -------------------------------------------------------------------------------- 1 | # Name of sample 2 | 3 | ## Requirements 4 | 5 | - Maven 6 | - Java 7 | - [Configured .env file](../README.md) 8 | 9 | 1. Build the jar 10 | 11 | ``` 12 | mvn package 13 | ``` 14 | 15 | 2. Run the packaged jar 16 | 17 | ``` 18 | java -cp target/sample-jar-with-dependencies.jar com.stripe.sample.Server 19 | ``` 20 | 21 | 3. Go to `localhost:4242` in your browser to see the demo 22 | -------------------------------------------------------------------------------- /server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.stripe.sample 7 | accept-a-payment 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-simple 15 | 2.0.9 16 | 17 | 18 | com.sparkjava 19 | spark-core 20 | 2.9.4 21 | 22 | 23 | com.google.code.gson 24 | gson 25 | 2.10.1 26 | 27 | 28 | com.stripe 29 | stripe-java 30 | 23.4.0 31 | 32 | 33 | io.github.cdimascio 34 | java-dotenv 35 | 5.2.2 36 | 37 | 38 | 39 | sample 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-compiler-plugin 44 | 3.11.0 45 | 46 | 1.8 47 | 1.8 48 | 49 | 50 | 51 | maven-assembly-plugin 52 | 53 | 54 | package 55 | 56 | single 57 | 58 | 59 | 60 | 61 | 62 | 63 | jar-with-dependencies 64 | 65 | 66 | 67 | Server 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/stripe/sample/Server.java: -------------------------------------------------------------------------------- 1 | package com.stripe.sample; 2 | 3 | import static spark.Spark.get; 4 | import static spark.Spark.port; 5 | import static spark.Spark.post; 6 | import static spark.Spark.staticFiles; 7 | 8 | import com.google.gson.Gson; 9 | import com.google.gson.annotations.SerializedName; 10 | import com.stripe.Stripe; 11 | import com.stripe.exception.*; 12 | import com.stripe.model.Event; 13 | import com.stripe.model.EventDataObjectDeserializer; 14 | import com.stripe.net.ApiResource; 15 | import com.stripe.net.Webhook; 16 | import io.github.cdimascio.dotenv.Dotenv; 17 | import java.nio.file.Paths; 18 | import java.util.HashMap; 19 | 20 | public class Server { 21 | 22 | private static Gson gson = new Gson(); 23 | 24 | static class ConfigResponse { 25 | 26 | private String publishableKey; 27 | 28 | public ConfigResponse(String publishableKey) { 29 | this.publishableKey = publishableKey; 30 | } 31 | } 32 | 33 | static class FailureResponse { 34 | 35 | private HashMap error; 36 | 37 | public FailureResponse(String message) { 38 | this.error = new HashMap(); 39 | this.error.put("message", message); 40 | } 41 | } 42 | 43 | public static void main(String[] args) { 44 | port(4242); 45 | Dotenv dotenv = Dotenv.load(); 46 | 47 | Stripe.apiKey = dotenv.get("STRIPE_SECRET_KEY"); 48 | 49 | // For sample support and debugging, not required for production: 50 | Stripe.setAppInfo( 51 | "stripe-samples/", 52 | "0.0.1", 53 | "https://github.com/stripe-samples" 54 | ); 55 | 56 | staticFiles.externalLocation( 57 | Paths 58 | .get( 59 | Paths.get("").toAbsolutePath().toString(), 60 | dotenv.get("STATIC_DIR") 61 | ) 62 | .normalize() 63 | .toString() 64 | ); 65 | 66 | get( 67 | "/config", 68 | (request, response) -> { 69 | response.type("application/json"); 70 | 71 | return gson.toJson( 72 | new ConfigResponse(dotenv.get("STRIPE_PUBLISHABLE_KEY")) 73 | ); 74 | } 75 | ); 76 | 77 | post( 78 | "/webhook", 79 | (request, response) -> { 80 | String payload = request.body(); 81 | String sigHeader = request.headers("Stripe-Signature"); 82 | String endpointSecret = dotenv.get("STRIPE_WEBHOOK_SECRET"); 83 | 84 | Event event = null; 85 | 86 | try { 87 | event = Webhook.constructEvent(payload, sigHeader, endpointSecret); 88 | } catch (SignatureVerificationException e) { 89 | // Invalid signature 90 | response.status(400); 91 | return ""; 92 | } 93 | 94 | switch (event.getType()) { 95 | case "payment_intent.succeeded": 96 | // Fulfill any orders, e-mail receipts, etc 97 | // To cancel the payment you will need to issue a Refund 98 | // (https://stripe.com/docs/api/refunds) 99 | System.out.println("💰Payment received!"); 100 | break; 101 | case "payment_intent.payment_failed": 102 | System.out.println("❌ Payment failed."); 103 | break; 104 | default: 105 | // Unexpected event type 106 | response.status(400); 107 | return ""; 108 | } 109 | 110 | response.status(200); 111 | return ""; 112 | } 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/node-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Name of sample 2 | 3 | A TypeScript [Express server](http://expressjs.com) implementation 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Install dependencies 13 | 14 | ``` 15 | npm install 16 | ``` 17 | 18 | 2. Run the application 19 | 20 | ``` 21 | npm start 22 | ``` 23 | 24 | 3. Go to `localhost:4242` to see the demo 25 | -------------------------------------------------------------------------------- /server/node-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-sample-demo", 3 | "version": "1.0.0", 4 | "description": "A Stripe demo", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "prebuild": "tslint -c tslint.json -p tsconfig.json --fix", 8 | "build": "tsc", 9 | "prestart": "npm run build", 10 | "start": "node .", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "stripe-demos", 14 | "license": "ISC", 15 | "dependencies": { 16 | "body-parser": "^1.19.0", 17 | "dotenv": "latest", 18 | "express": "^4.17.1", 19 | "stripe": "13.5.0" 20 | }, 21 | "devDependencies": { 22 | "@types/express": "^4.17.2", 23 | "@types/node": "^20.5.9", 24 | "tslint": "^6.1.3", 25 | "typescript": "^5.2.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/node-typescript/src/server.ts: -------------------------------------------------------------------------------- 1 | import env from 'dotenv'; 2 | import path from 'path'; 3 | 4 | // Replace if using a different env file or config. 5 | env.config({ path: './.env' }); 6 | 7 | import bodyParser from 'body-parser'; 8 | import express from 'express'; 9 | import Stripe from 'stripe'; 10 | 11 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { 12 | apiVersion: '2020-08-27', 13 | appInfo: { // For sample support and debugging, not required for production: 14 | name: 'stripe-samples/', 15 | url: 'https://github.com/stripe-samples', 16 | version: '0.0.1', 17 | }, 18 | typescript: true, 19 | }); 20 | 21 | const app = express(); 22 | const resolve = path.resolve; 23 | 24 | app.use(express.static(process.env.STATIC_DIR)); 25 | app.use( 26 | ( 27 | req: express.Request, 28 | res: express.Response, 29 | next: express.NextFunction 30 | ): void => { 31 | if (req.originalUrl === '/webhook') { 32 | next(); 33 | } else { 34 | bodyParser.json()(req, res, next); 35 | } 36 | } 37 | ); 38 | 39 | app.get('/', (_: express.Request, res: express.Response): void => { 40 | // Serve checkout page. 41 | const indexPath = resolve(process.env.STATIC_DIR + '/index.html'); 42 | res.sendFile(indexPath); 43 | }); 44 | 45 | app.get('/config', (_: express.Request, res: express.Response): void => { 46 | // Serve checkout page. 47 | res.send({ 48 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY 49 | }); 50 | }); 51 | 52 | // Expose a endpoint as a webhook handler for asynchronous events. 53 | // Configure your webhook in the stripe developer dashboard: 54 | // https://dashboard.stripe.com/test/webhooks 55 | app.post( 56 | '/webhook', 57 | // Use body-parser to retrieve the raw body as a buffer. 58 | bodyParser.raw({ type: 'application/json' }), 59 | async (req: express.Request, res: express.Response): Promise => { 60 | // Retrieve the event by verifying the signature using the raw body and secret. 61 | let event: Stripe.Event; 62 | 63 | try { 64 | event = stripe.webhooks.constructEvent( 65 | req.body, 66 | req.headers['stripe-signature'], 67 | process.env.STRIPE_WEBHOOK_SECRET 68 | ); 69 | } catch (err) { 70 | console.log(`⚠️ Webhook signature verification failed.`); 71 | res.sendStatus(400); 72 | return; 73 | } 74 | 75 | // Extract the data from the event. 76 | const data: Stripe.Event.Data = event.data; 77 | const eventType: string = event.type; 78 | 79 | if (eventType === 'payment_intent.succeeded') { 80 | // Cast the event into a PaymentIntent to make use of the types. 81 | const pi: Stripe.PaymentIntent = data.object as Stripe.PaymentIntent; 82 | // Funds have been captured 83 | // Fulfill any orders, e-mail receipts, etc 84 | // To cancel the payment after capture you will need to issue a Refund (https://stripe.com/docs/api/refunds). 85 | console.log(`🔔 Webhook received: ${pi.object} ${pi.status}!`); 86 | console.log('💰 Payment captured!'); 87 | } else if (eventType === 'payment_intent.payment_failed') { 88 | // Cast the event into a PaymentIntent to make use of the types. 89 | const pi: Stripe.PaymentIntent = data.object as Stripe.PaymentIntent; 90 | console.log(`🔔 Webhook received: ${pi.object} ${pi.status}!`); 91 | console.log('❌ Payment failed.'); 92 | } 93 | res.sendStatus(200); 94 | } 95 | ); 96 | 97 | app.listen(4242, (): void => 98 | console.log(`Node server listening on port ${4242}!`) 99 | ); 100 | -------------------------------------------------------------------------------- /server/node-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": ["node_modules/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /server/node-typescript/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": { 6 | "trailing-comma": [false], 7 | "no-console": false, 8 | "quotemark": [true, "single"] 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | -------------------------------------------------------------------------------- /server/node/README.md: -------------------------------------------------------------------------------- 1 | # Name of sample 2 | 3 | An [Express server](http://expressjs.com) implementation 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Install dependencies 13 | 14 | ``` 15 | npm install 16 | ``` 17 | 18 | 2. Run the application 19 | 20 | ``` 21 | npm start 22 | ``` 23 | 24 | 3. Go to `localhost:4242` to see the demo 25 | -------------------------------------------------------------------------------- /server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-sample-demo", 3 | "version": "1.0.0", 4 | "description": "A Stripe demo", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "stripe-demos", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "dotenv": "^16.0.0", 15 | "express": "^4.17.1", 16 | "stripe": "^13.5.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/node/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | // Replace if using a different env file or config 5 | const env = require('dotenv').config({ path: './.env' }); 6 | 7 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 8 | apiVersion: '2020-08-27', 9 | appInfo: { // For sample support and debugging, not required for production: 10 | name: "stripe-samples/", 11 | version: "0.0.1", 12 | url: "https://github.com/stripe-samples" 13 | } 14 | }); 15 | 16 | app.use(express.static(process.env.STATIC_DIR)); 17 | app.use( 18 | express.json({ 19 | // We need the raw body to verify webhook signatures. 20 | // Let's compute it only when hitting the Stripe webhook endpoint. 21 | verify: function(req, res, buf) { 22 | if (req.originalUrl.startsWith('/webhook')) { 23 | req.rawBody = buf.toString(); 24 | } 25 | } 26 | }) 27 | ); 28 | 29 | app.get('/', (req, res) => { 30 | const path = resolve(process.env.STATIC_DIR + '/index.html'); 31 | res.sendFile(path); 32 | }); 33 | 34 | app.get('/config', (req, res) => { 35 | res.send({ 36 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 37 | }); 38 | }); 39 | 40 | // Expose a endpoint as a webhook handler for asynchronous events. 41 | // Configure your webhook in the stripe developer dashboard 42 | // https://dashboard.stripe.com/test/webhooks 43 | app.post('/webhook', async (req, res) => { 44 | let data, eventType; 45 | 46 | // Check if webhook signing is configured. 47 | if (process.env.STRIPE_WEBHOOK_SECRET) { 48 | // Retrieve the event by verifying the signature using the raw body and secret. 49 | let event; 50 | let signature = req.headers['stripe-signature']; 51 | try { 52 | event = stripe.webhooks.constructEvent( 53 | req.rawBody, 54 | signature, 55 | process.env.STRIPE_WEBHOOK_SECRET 56 | ); 57 | } catch (err) { 58 | console.log(`⚠️ Webhook signature verification failed.`); 59 | return res.sendStatus(400); 60 | } 61 | data = event.data; 62 | eventType = event.type; 63 | } else { 64 | // Webhook signing is recommended, but if the secret is not configured in `config.js`, 65 | // we can retrieve the event data directly from the request body. 66 | data = req.body.data; 67 | eventType = req.body.type; 68 | } 69 | 70 | if (eventType === 'payment_intent.succeeded') { 71 | // Funds have been captured 72 | // Fulfill any orders, e-mail receipts, etc 73 | // To cancel the payment after capture you will need to issue a Refund (https://stripe.com/docs/api/refunds) 74 | console.log('💰 Payment captured!'); 75 | } else if (eventType === 'payment_intent.payment_failed') { 76 | console.log('❌ Payment failed.'); 77 | } 78 | res.sendStatus(200); 79 | }); 80 | 81 | app.listen(4242, () => console.log(`Node server listening at http://localhost:4242`)); 82 | -------------------------------------------------------------------------------- /server/php/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] 6 | -------------------------------------------------------------------------------- /server/php/README.md: -------------------------------------------------------------------------------- 1 | # Your sample name 2 | 3 | An implementation in PHP 4 | 5 | 6 | ## Requirements 7 | 8 | - PHP 9 | 10 | ## How to run 11 | 12 | 1. Confirm `.env` configuration 13 | 14 | Ensure the API keys are configured in `.env` in this directory. It should include the following keys: 15 | 16 | ```yaml 17 | # Stripe API keys - see https://stripe.com/docs/development/quickstart#api-keys 18 | STRIPE_PUBLISHABLE_KEY=pk_test... 19 | STRIPE_SECRET_KEY=sk_test... 20 | 21 | # Required to verify signatures in the webhook handler. 22 | # See README on how to use the Stripe CLI to test webhooks 23 | STRIPE_WEBHOOK_SECRET=whsec_... 24 | 25 | # Path to front-end implementation. Note: PHP has it's own front end implementation. 26 | STATIC_DIR=../../client/html 27 | ``` 28 | 29 | 2. Run composer to set up dependencies 30 | 31 | ``` 32 | composer install 33 | ``` 34 | 35 | 3. Copy .env.example to .env and replace with your Stripe API keys 36 | 37 | ``` 38 | cp ../../.env.example .env 39 | ``` 40 | 41 | 4. Run the server locally 42 | 43 | ``` 44 | cd public 45 | php -S 127.0.0.1:4242 46 | ``` 47 | 48 | 4. Go to [localhost:4242](http://localhost:4242) 49 | -------------------------------------------------------------------------------- /server/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "stripe/stripe-php": "^12.2", 4 | "vlucas/phpdotenv": "^5.3" 5 | }, 6 | "scripts": { 7 | "start": "cd public && php -S 0.0.0.0:4242" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/php/public/css/global.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --gray-offset: rgba(0, 0, 0, 0.03); 4 | --gray-border: rgba(0, 0, 0, 0.15); 5 | --gray-light: rgba(0, 0, 0, 0.4); 6 | --gray-mid: rgba(0, 0, 0, 0.7); 7 | --gray-dark: rgba(0, 0, 0, 0.9); 8 | --body-color: var(--gray-mid); 9 | --headline-color: var(--gray-dark); 10 | --accent-color: #0066f0; 11 | --body-font-family: -apple-system, BlinkMacSystemFont, sans-serif; 12 | --radius: 6px; 13 | --form-width: 343px; 14 | } 15 | 16 | /* Base */ 17 | * { 18 | box-sizing: border-box; 19 | } 20 | body { 21 | font-family: var(--body-font-family); 22 | font-size: 16px; 23 | color: var(--body-color); 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6 { 32 | color: var(--body-color); 33 | margin-top: 2px; 34 | margin-bottom: 4px; 35 | } 36 | h1 { 37 | font-size: 27px; 38 | color: var(--headline-color); 39 | } 40 | h4 { 41 | font-weight: 500; 42 | font-size: 14px; 43 | color: var(--gray-light); 44 | } 45 | 46 | /* Layout */ 47 | .sr-root { 48 | display: flex; 49 | flex-direction: row; 50 | width: 100%; 51 | max-width: 980px; 52 | padding: 48px; 53 | align-content: center; 54 | justify-content: center; 55 | height: auto; 56 | min-height: 100vh; 57 | margin: 0 auto; 58 | } 59 | .sr-header { 60 | margin-bottom: 32px; 61 | } 62 | .sr-payment-summary { 63 | margin-bottom: 20px; 64 | } 65 | .sr-main, 66 | .sr-content { 67 | display: flex; 68 | flex-direction: column; 69 | justify-content: center; 70 | height: 100%; 71 | align-self: center; 72 | } 73 | .sr-main { 74 | width: var(--form-width); 75 | } 76 | .sr-content { 77 | padding-left: 48px; 78 | } 79 | .sr-header__logo { 80 | background-image: var(--logo-image); 81 | height: 24px; 82 | background-size: contain; 83 | background-repeat: no-repeat; 84 | width: 100%; 85 | } 86 | .sr-legal-text { 87 | color: var(--gray-light); 88 | text-align: center; 89 | font-size: 13px; 90 | line-height: 17px; 91 | margin-top: 12px; 92 | } 93 | .sr-field-error { 94 | color: var(--accent-color); 95 | text-align: left; 96 | font-size: 13px; 97 | line-height: 17px; 98 | margin-top: 12px; 99 | } 100 | 101 | /* Form */ 102 | .sr-form-row { 103 | margin: 16px 0; 104 | } 105 | label { 106 | font-size: 13px; 107 | font-weight: 500; 108 | margin-bottom: 8px; 109 | display: inline-block; 110 | } 111 | 112 | /* Inputs */ 113 | .sr-input, 114 | .sr-select, 115 | input[type="text"] { 116 | border: 1px solid var(--gray-border); 117 | border-radius: var(--radius); 118 | padding: 5px 12px; 119 | height: 44px; 120 | width: 100%; 121 | transition: box-shadow 0.2s ease; 122 | background: white; 123 | -moz-appearance: none; 124 | -webkit-appearance: none; 125 | appearance: none; 126 | color: #32325d; 127 | } 128 | .sr-input:focus, 129 | input[type="text"]:focus, 130 | button:focus, 131 | .focused { 132 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 133 | 0 0 0 4px rgba(50, 151, 211, 0.3); 134 | outline: none; 135 | z-index: 9; 136 | } 137 | .sr-input::placeholder, 138 | input[type="text"]::placeholder { 139 | color: var(--gray-light); 140 | } 141 | 142 | /* Checkbox */ 143 | .sr-checkbox-label { 144 | position: relative; 145 | cursor: pointer; 146 | } 147 | 148 | .sr-checkbox-label input { 149 | opacity: 0; 150 | margin-right: 6px; 151 | } 152 | 153 | .sr-checkbox-label .sr-checkbox-check { 154 | position: absolute; 155 | left: 0; 156 | height: 16px; 157 | width: 16px; 158 | background-color: white; 159 | border: 1px solid var(--gray-border); 160 | border-radius: 4px; 161 | transition: all 0.2s ease; 162 | } 163 | 164 | .sr-checkbox-label input:focus ~ .sr-checkbox-check { 165 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 166 | 0 0 0 4px rgba(50, 151, 211, 0.3); 167 | outline: none; 168 | } 169 | 170 | .sr-checkbox-label input:checked ~ .sr-checkbox-check { 171 | background-color: var(--accent-color); 172 | background-repeat: no-repeat; 173 | background-size: 16px; 174 | background-position: -1px -1px; 175 | } 176 | 177 | /* Select */ 178 | .sr-select { 179 | display: block; 180 | height: 44px; 181 | margin: 0; 182 | background-repeat: no-repeat, repeat; 183 | background-position: right 12px top 50%, 0 0; 184 | background-size: 0.65em auto, 100%; 185 | } 186 | .sr-select::-ms-expand { 187 | display: none; 188 | } 189 | .sr-select:hover { 190 | cursor: pointer; 191 | } 192 | .sr-select:focus { 193 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 194 | 0 0 0 4px rgba(50, 151, 211, 0.3); 195 | outline: none; 196 | } 197 | .sr-select option { 198 | font-weight: 400; 199 | } 200 | .sr-select:invalid { 201 | color: var(--gray-light); 202 | } 203 | 204 | /* Combo inputs */ 205 | .sr-combo-inputs { 206 | display: flex; 207 | flex-direction: column; 208 | } 209 | .sr-combo-inputs input, 210 | .sr-combo-inputs .sr-select { 211 | border-radius: 0; 212 | border-bottom: 0; 213 | } 214 | .sr-combo-inputs > input:first-child, 215 | .sr-combo-inputs > .sr-select:first-child { 216 | border-radius: var(--radius) var(--radius) 0 0; 217 | } 218 | .sr-combo-inputs > input:last-child, 219 | .sr-combo-inputs > .sr-select:last-child { 220 | border-radius: 0 0 var(--radius) var(--radius); 221 | border-bottom: 1px solid var(--gray-border); 222 | } 223 | .sr-combo-inputs > .sr-combo-inputs-row:last-child input:first-child { 224 | border-radius: 0 0 0 var(--radius); 225 | border-bottom: 1px solid var(--gray-border); 226 | } 227 | .sr-combo-inputs > .sr-combo-inputs-row:last-child input:last-child { 228 | border-radius: 0 0 var(--radius) 0; 229 | border-bottom: 1px solid var(--gray-border); 230 | } 231 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:first-child { 232 | border-radius: var(--radius) 0 0 0; 233 | } 234 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:last-child { 235 | border-radius: 0 var(--radius) 0 0; 236 | } 237 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:only-child { 238 | border-radius: var(--radius) var(--radius) 0 0; 239 | } 240 | .sr-combo-inputs-row { 241 | width: 100%; 242 | display: flex; 243 | } 244 | 245 | .sr-combo-inputs-row > input { 246 | width: 100%; 247 | border-radius: 0; 248 | } 249 | 250 | .sr-combo-inputs-row > input:first-child:not(:only-child) { 251 | border-right: 0; 252 | } 253 | 254 | .sr-combo-inputs-row:not(:first-of-type) .sr-input { 255 | border-radius: 0 0 var(--radius) var(--radius); 256 | } 257 | 258 | .sr-result { 259 | height: 44px; 260 | -webkit-transition: height 1s ease; 261 | -moz-transition: height 1s ease; 262 | -o-transition: height 1s ease; 263 | transition: height 1s ease; 264 | color: var(--font-color); 265 | overflow: auto; 266 | } 267 | .sr-result code { 268 | overflow: scroll; 269 | } 270 | .sr-result.expand { 271 | height: 350px; 272 | } 273 | 274 | /* Buttons and links */ 275 | button { 276 | background: var(--accent-color); 277 | border-radius: var(--radius); 278 | color: white; 279 | border: 0; 280 | padding: 12px 16px; 281 | margin-top: 16px; 282 | font-weight: 600; 283 | cursor: pointer; 284 | transition: all 0.2s ease; 285 | display: block; 286 | } 287 | button:hover { 288 | filter: contrast(115%); 289 | } 290 | button:active { 291 | transform: translateY(0px) scale(0.98); 292 | filter: brightness(0.9); 293 | } 294 | button:disabled { 295 | opacity: 0.5; 296 | cursor: not-allowed; 297 | } 298 | 299 | .sr-payment-form button, 300 | .fullwidth { 301 | width: 100%; 302 | } 303 | 304 | a { 305 | color: var(--accent-color); 306 | text-decoration: none; 307 | transition: all 0.2s ease; 308 | } 309 | 310 | a:hover { 311 | filter: brightness(0.8); 312 | } 313 | 314 | a:active { 315 | filter: brightness(0.5); 316 | } 317 | 318 | /* Code block */ 319 | .sr-callout { 320 | background: var(--gray-offset); 321 | padding: 12px; 322 | border-radius: var(--radius); 323 | max-height: 200px; 324 | overflow: auto; 325 | } 326 | code, 327 | pre { 328 | font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace; 329 | font-size: 12px; 330 | } 331 | 332 | /* Stripe Element placeholder */ 333 | .sr-card-element { 334 | padding-top: 12px; 335 | } 336 | 337 | /* Responsiveness */ 338 | @media (max-width: 720px) { 339 | .sr-root { 340 | flex-direction: column; 341 | justify-content: flex-start; 342 | padding: 48px 20px; 343 | min-width: 320px; 344 | } 345 | 346 | .sr-header__logo { 347 | background-position: center; 348 | } 349 | 350 | .sr-payment-summary { 351 | text-align: center; 352 | } 353 | 354 | .sr-content { 355 | display: none; 356 | } 357 | 358 | .sr-main { 359 | width: 100%; 360 | } 361 | } 362 | 363 | /* Pasha styles – Brand-overrides, can split these out */ 364 | :root { 365 | --accent-color: #ed5f74; 366 | --headline-color: var(--accent-color); 367 | } 368 | 369 | .pasha-image-stack { 370 | display: grid; 371 | grid-gap: 12px; 372 | grid-template-columns: auto auto; 373 | } 374 | 375 | .pasha-image-stack img { 376 | border-radius: var(--radius); 377 | background-color: var(--gray-border); 378 | box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 379 | 0 3px 6px 0 rgba(0, 0, 0, 0.07); 380 | transition: all 0.8s ease; 381 | opacity: 0; 382 | } 383 | 384 | .pasha-image-stack img:nth-child(1) { 385 | transform: translate(12px, -12px); 386 | opacity: 1; 387 | } 388 | .pasha-image-stack img:nth-child(2) { 389 | transform: translate(-24px, 16px); 390 | opacity: 1; 391 | } 392 | .pasha-image-stack img:nth-child(3) { 393 | transform: translate(68px, -100px); 394 | opacity: 1; 395 | } 396 | 397 | /* todo: spinner/processing state, errors, animations */ 398 | 399 | .spinner, 400 | .spinner:before, 401 | .spinner:after { 402 | border-radius: 50%; 403 | } 404 | .spinner { 405 | color: #ffffff; 406 | font-size: 22px; 407 | text-indent: -99999px; 408 | margin: 0px auto; 409 | position: relative; 410 | width: 20px; 411 | height: 20px; 412 | box-shadow: inset 0 0 0 2px; 413 | -webkit-transform: translateZ(0); 414 | -ms-transform: translateZ(0); 415 | transform: translateZ(0); 416 | } 417 | .spinner:before, 418 | .spinner:after { 419 | position: absolute; 420 | content: ""; 421 | } 422 | .spinner:before { 423 | width: 10.4px; 424 | height: 20.4px; 425 | background: var(--accent-color); 426 | border-radius: 20.4px 0 0 20.4px; 427 | top: -0.2px; 428 | left: -0.2px; 429 | -webkit-transform-origin: 10.4px 10.2px; 430 | transform-origin: 10.4px 10.2px; 431 | -webkit-animation: loading 2s infinite ease 1.5s; 432 | animation: loading 2s infinite ease 1.5s; 433 | } 434 | .spinner:after { 435 | width: 10.4px; 436 | height: 10.2px; 437 | background: var(--accent-color); 438 | border-radius: 0 10.2px 10.2px 0; 439 | top: -0.1px; 440 | left: 10.2px; 441 | -webkit-transform-origin: 0px 10.2px; 442 | transform-origin: 0px 10.2px; 443 | -webkit-animation: loading 2s infinite ease; 444 | animation: loading 2s infinite ease; 445 | } 446 | @-webkit-keyframes loading { 447 | 0% { 448 | -webkit-transform: rotate(0deg); 449 | transform: rotate(0deg); 450 | } 451 | 100% { 452 | -webkit-transform: rotate(360deg); 453 | transform: rotate(360deg); 454 | } 455 | } 456 | @keyframes loading { 457 | 0% { 458 | -webkit-transform: rotate(0deg); 459 | transform: rotate(0deg); 460 | } 461 | 100% { 462 | -webkit-transform: rotate(360deg); 463 | transform: rotate(360deg); 464 | } 465 | } 466 | 467 | /* Animated form */ 468 | 469 | .sr-root { 470 | animation: 0.4s form-in; 471 | animation-fill-mode: both; 472 | animation-timing-function: ease; 473 | } 474 | 475 | .sr-payment-form .sr-form-row { 476 | animation: 0.4s field-in; 477 | animation-fill-mode: both; 478 | animation-timing-function: ease; 479 | transform-origin: 50% 0%; 480 | } 481 | 482 | /* need saas for loop :D */ 483 | .sr-payment-form .sr-form-row:nth-child(1) { 484 | animation-delay: 0; 485 | } 486 | .sr-payment-form .sr-form-row:nth-child(2) { 487 | animation-delay: 60ms; 488 | } 489 | .sr-payment-form .sr-form-row:nth-child(3) { 490 | animation-delay: 120ms; 491 | } 492 | .sr-payment-form .sr-form-row:nth-child(4) { 493 | animation-delay: 180ms; 494 | } 495 | .sr-payment-form .sr-form-row:nth-child(5) { 496 | animation-delay: 240ms; 497 | } 498 | .sr-payment-form .sr-form-row:nth-child(6) { 499 | animation-delay: 300ms; 500 | } 501 | .hidden { 502 | display: none; 503 | } 504 | 505 | @keyframes field-in { 506 | 0% { 507 | opacity: 0; 508 | transform: translateY(8px) scale(0.95); 509 | } 510 | 100% { 511 | opacity: 1; 512 | transform: translateY(0px) scale(1); 513 | } 514 | } 515 | 516 | @keyframes form-in { 517 | 0% { 518 | opacity: 0; 519 | transform: scale(0.98); 520 | } 521 | 100% { 522 | opacity: 1; 523 | transform: scale(1); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /server/php/public/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } -------------------------------------------------------------------------------- /server/php/public/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Stripe Sample 8 | 9 | 10 | 11 | 12 |
13 |

Stripe Sample

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /server/php/public/shared.php: -------------------------------------------------------------------------------- 1 | 8 |

Missing .env

9 | 10 |

Make a copy of .env.example, place it in the same directory as composer.json, and name it .env, then populate the variables.

11 |

It should look something like the following, but contain your API keys:

12 |
STRIPE_PUBLISHABLE_KEY=pk_test...
13 | STRIPE_SECRET_KEY=sk_test...
14 | STRIPE_WEBHOOK_SECRET=whsec_...
15 | DOMAIN=http://localhost:4242
16 |
17 | 18 |

You can use this command to get started:

19 |
cp .env.example .env
20 | 21 | load(); 30 | 31 | // Make sure the configuration file is good. 32 | if (!$_ENV['STRIPE_SECRET_KEY']) { 33 | ?> 34 | 35 |

Invalid .env

36 |

Make a copy of .env.example and name it .env, then populate the variables.

37 |

It should look something like the following, but contain your API keys:

38 |
STRIPE_PUBLISHABLE_KEY=pk_test...
39 | STRIPE_SECRET_KEY=sk_test...
40 | STRIPE_WEBHOOK_SECRET=whsec_...
41 | DOMAIN=http://localhost:4242
42 |
43 | 44 |

You can use this command to get started:

45 |
cp .env.example .env
46 | 47 | ", 54 | "0.0.2", 55 | "https://github.com/stripe-samples" 56 | ); 57 | 58 | $stripe = new \Stripe\StripeClient([ 59 | 'api_key' => $_ENV['STRIPE_SECRET_KEY'], 60 | 'stripe_version' => '2022-08-01', 61 | ]); 62 | -------------------------------------------------------------------------------- /server/php/public/utils.js: -------------------------------------------------------------------------------- 1 | // Helper for displaying status messages. 2 | const addMessage = (message) => { 3 | const messagesDiv = document.querySelector('#messages'); 4 | messagesDiv.style.display = 'block'; 5 | const messageWithLinks = addDashboardLinks(message); 6 | messagesDiv.innerHTML += `> ${messageWithLinks}
`; 7 | console.log(`Debug: ${message}`); 8 | }; 9 | 10 | // Adds links for known Stripe objects to the Stripe dashboard. 11 | const addDashboardLinks = (message) => { 12 | const piDashboardBase = 'https://dashboard.stripe.com/test/payments'; 13 | return message.replace( 14 | /(pi_(\S*)\b)/g, 15 | `$1` 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /server/php/public/webhook.php: -------------------------------------------------------------------------------- 1 | $e->getMessage() ]); 21 | exit; 22 | } 23 | 24 | if ($event->type == 'payment_intent.succeeded') { 25 | // Fulfill any orders, e-mail receipts, etc 26 | // To cancel the payment you will need to issue a Refund (https://stripe.com/docs/api/refunds) 27 | error_log('💰 Payment received!'); 28 | } 29 | else if ($event->type == 'payment_intent.payment_failed') { 30 | error_log('❌ Payment failed.'); 31 | } 32 | 33 | echo json_encode(['status' => 'success']); 34 | -------------------------------------------------------------------------------- /server/python/README.md: -------------------------------------------------------------------------------- 1 | # Name of sample 2 | 3 | ## Requirements## Requirements 4 | 5 | - Python 3 6 | - [Configured .env file](../README.md) 7 | 8 | ## How to run 9 | 10 | 1. Create and activate a new virtual environment 11 | 12 | **MacOS / Unix** 13 | 14 | ``` 15 | python3 -m venv env 16 | source env/bin/activate 17 | ``` 18 | 19 | **Windows (PowerShell)** 20 | 21 | ``` 22 | python3 -m venv env 23 | .\env\Scripts\activate.bat 24 | ``` 25 | 26 | 2. Install dependencies 27 | 28 | ``` 29 | pip install -r requirements.txt 30 | ``` 31 | 32 | 3. Export and run the application 33 | 34 | **MacOS / Unix** 35 | 36 | ``` 37 | export FLASK_APP=server.py 38 | python3 -m flask run --port=4242 39 | ``` 40 | 41 | **Windows (PowerShell)** 42 | 43 | ``` 44 | $env:FLASK_APP=“server.py" 45 | python3 -m flask run --port=4242 46 | ``` 47 | 48 | 4. Go to `localhost:4242` in your browser to see the demo 49 | -------------------------------------------------------------------------------- /server/python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | python-dotenv==1.0.0 3 | stripe==6.4.0 4 | -------------------------------------------------------------------------------- /server/python/server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3.6 2 | import stripe 3 | import json 4 | import os 5 | 6 | from flask import Flask, render_template, jsonify, request, send_from_directory 7 | from dotenv import load_dotenv, find_dotenv 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | # For sample support and debugging, not required for production: 12 | stripe.set_app_info( 13 | 'stripe-samples/your-sample-name', 14 | version='0.0.1', 15 | url='https://github.com/stripe-samples') 16 | 17 | stripe.api_version = '2020-08-27' 18 | stripe.api_key = os.getenv('STRIPE_SECRET_KEY') 19 | 20 | static_dir = str(os.path.abspath(os.path.join(__file__ , "..", os.getenv("STATIC_DIR")))) 21 | app = Flask(__name__, static_folder=static_dir, static_url_path="", template_folder=static_dir) 22 | 23 | @app.route('/', methods=['GET']) 24 | def get_root(): 25 | return render_template('index.html') 26 | 27 | 28 | @app.route('/config', methods=['GET']) 29 | def get_config(): 30 | return jsonify({'publishableKey': os.getenv('STRIPE_PUBLISHABLE_KEY')}) 31 | 32 | 33 | @app.route('/webhook', methods=['POST']) 34 | def webhook_received(): 35 | # You can use webhooks to receive information about asynchronous payment events. 36 | # For more about our webhook events check out https://stripe.com/docs/webhooks. 37 | webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET') 38 | request_data = json.loads(request.data) 39 | 40 | if webhook_secret: 41 | # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured. 42 | signature = request.headers.get('stripe-signature') 43 | try: 44 | event = stripe.Webhook.construct_event( 45 | payload=request.data, sig_header=signature, secret=webhook_secret) 46 | data = event['data'] 47 | except Exception as e: 48 | return e 49 | # Get the type of webhook event sent - used to check the status of PaymentIntents. 50 | event_type = event['type'] 51 | else: 52 | data = request_data['data'] 53 | event_type = request_data['type'] 54 | data_object = data['object'] 55 | 56 | if event_type == 'payment_intent.succeeded': 57 | print('💰 Payment received!') 58 | 59 | 60 | return jsonify({'status': 'success'}) 61 | 62 | 63 | if __name__ == '__main__': 64 | app.run(port=4242, debug=True) 65 | -------------------------------------------------------------------------------- /server/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | 3 | gem 'dotenv' 4 | gem 'json' 5 | gem 'sinatra' 6 | gem 'stripe', '9.2.0' 7 | gem 'webrick' 8 | -------------------------------------------------------------------------------- /server/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Name of sample 2 | 3 | A [Sinatra](http://sinatrarb.com/) implementation. 4 | 5 | ## Requirements 6 | 7 | - Ruby v2.4.5+ 8 | - [Configured .env file](../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Install dependencies 13 | 14 | ``` 15 | bundle install 16 | ``` 17 | 18 | 2. Run the application 19 | 20 | ``` 21 | ruby server.rb 22 | ``` 23 | 24 | 3. Go to `http://localhost:4242` in your browser to see the demo 25 | -------------------------------------------------------------------------------- /server/ruby/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'stripe' 4 | require 'sinatra' 5 | require 'dotenv' 6 | 7 | # Replace if using a different env file or config 8 | Dotenv.load 9 | 10 | # For sample support and debugging, not required for production: 11 | Stripe.set_app_info( 12 | 'stripe-samples//[]', 13 | version: '0.0.1', 14 | url: 'https://github.com/stripe-samples' 15 | ) 16 | Stripe.api_version = '2020-08-27' 17 | Stripe.api_key = ENV['STRIPE_SECRET_KEY'] 18 | 19 | set :static, true 20 | set :public_folder, File.join(File.dirname(__FILE__), ENV['STATIC_DIR']) 21 | set :port, 4242 22 | 23 | get '/' do 24 | content_type 'text/html' 25 | send_file File.join(settings.public_folder, 'index.html') 26 | end 27 | 28 | get '/config' do 29 | content_type 'application/json' 30 | { 31 | publishableKey: ENV['STRIPE_PUBLISHABLE_KEY'], 32 | }.to_json 33 | end 34 | 35 | post '/webhook' do 36 | # You can use webhooks to receive information about asynchronous payment events. 37 | # For more about our webhook events check out https://stripe.com/docs/webhooks. 38 | webhook_secret = ENV['STRIPE_WEBHOOK_SECRET'] 39 | payload = request.body.read 40 | if !webhook_secret.empty? 41 | # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured. 42 | sig_header = request.env['HTTP_STRIPE_SIGNATURE'] 43 | event = nil 44 | 45 | begin 46 | event = Stripe::Webhook.construct_event( 47 | payload, sig_header, webhook_secret 48 | ) 49 | rescue JSON::ParserError => e 50 | # Invalid payload 51 | status 400 52 | return 53 | rescue Stripe::SignatureVerificationError => e 54 | # Invalid signature 55 | puts '⚠️ Webhook signature verification failed.' 56 | status 400 57 | return 58 | end 59 | else 60 | data = JSON.parse(payload, symbolize_names: true) 61 | event = Stripe::Event.construct_from(data) 62 | end 63 | 64 | case event.type 65 | when 'some.event' 66 | puts '🔔 Webhook received!' 67 | end 68 | 69 | content_type 'application/json' 70 | { 71 | status: 'success' 72 | }.to_json 73 | end 74 | -------------------------------------------------------------------------------- /spec/server_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative './spec_helper.rb' 2 | 3 | RSpec.describe " integration" do 4 | it "serves the index route" do 5 | # Get the index html page 6 | response = get("/") 7 | expect(response).not_to be_nil 8 | end 9 | 10 | it "serves config with publishableKey" do 11 | resp = get_json("/config") 12 | expect(resp).to have_key("publishableKey") 13 | expect(resp['publishableKey']).to start_with("pk_test") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'byebug' 2 | require 'json' 3 | require 'rest-client' 4 | require 'stripe' 5 | require 'dotenv' 6 | 7 | # This file was generated by the `rspec --init` command. Conventionally, all 8 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 9 | # The generated `.rspec` file contains `--require spec_helper` which will cause 10 | # this file to always be loaded, without a need to explicitly require it in any 11 | # files. 12 | # 13 | # Given that it is always loaded, you are encouraged to keep this file as 14 | # light-weight as possible. Requiring heavyweight dependencies from this file 15 | # will add to the boot time of your test suite on EVERY test run, even for an 16 | # individual file that may not need all of that loaded. Instead, consider making 17 | # a separate helper file that requires the additional dependencies and performs 18 | # the additional setup, and require it from the spec files that actually need 19 | # it. 20 | # 21 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 22 | RSpec.configure do |config| 23 | # rspec-expectations config goes here. You can use an alternate 24 | # assertion/expectation library such as wrong or the stdlib/minitest 25 | # assertions if you prefer. 26 | config.expect_with :rspec do |expectations| 27 | # This option will default to `true` in RSpec 4. It makes the `description` 28 | # and `failure_message` of custom matchers include text for helper methods 29 | # defined using `chain`, e.g.: 30 | # be_bigger_than(2).and_smaller_than(4).description 31 | # # => "be bigger than 2 and smaller than 4" 32 | # ...rather than: 33 | # # => "be bigger than 2" 34 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 35 | end 36 | 37 | # rspec-mocks config goes here. You can use an alternate test double 38 | # library (such as bogus or mocha) by changing the `mock_with` option here. 39 | config.mock_with :rspec do |mocks| 40 | # Prevents you from mocking or stubbing a method that does not exist on 41 | # a real object. This is generally recommended, and will default to 42 | # `true` in RSpec 4. 43 | mocks.verify_partial_doubles = true 44 | end 45 | 46 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 47 | # have no way to turn it off -- the option exists only for backwards 48 | # compatibility in RSpec 3). It causes shared context metadata to be 49 | # inherited by the metadata hash of host groups and examples, rather than 50 | # triggering implicit auto-inclusion in groups with matching metadata. 51 | config.shared_context_metadata_behavior = :apply_to_host_groups 52 | 53 | # The settings below are suggested to provide a good initial experience 54 | # with RSpec, but feel free to customize to your heart's content. 55 | =begin 56 | # This allows you to limit a spec run to individual examples or groups 57 | # you care about by tagging them with `:focus` metadata. When nothing 58 | # is tagged with `:focus`, all examples get run. RSpec also provides 59 | # aliases for `it`, `describe`, and `context` that include `:focus` 60 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 61 | config.filter_run_when_matching :focus 62 | 63 | # Allows RSpec to persist some state between runs in order to support 64 | # the `--only-failures` and `--next-failure` CLI options. We recommend 65 | # you configure your source control system to ignore this file. 66 | config.example_status_persistence_file_path = "spec/examples.txt" 67 | 68 | # Limits the available syntax to the non-monkey patched syntax that is 69 | # recommended. For more details, see: 70 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 71 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 72 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 73 | config.disable_monkey_patching! 74 | 75 | # This setting enables warnings. It's recommended, but in some cases may 76 | # be too noisy due to issues in dependencies. 77 | config.warnings = true 78 | 79 | # Many RSpec users commonly either run the entire suite or an individual 80 | # file, and it's useful to allow more verbose output when running an 81 | # individual spec file. 82 | if config.files_to_run.one? 83 | # Use the documentation formatter for detailed output, 84 | # unless a formatter has already been configured 85 | # (e.g. via a command-line flag). 86 | config.default_formatter = "doc" 87 | end 88 | 89 | # Print the 10 slowest examples and example groups at the 90 | # end of the spec run, to help surface which specs are running 91 | # particularly slow. 92 | config.profile_examples = 10 93 | 94 | # Run specs in random order to surface order dependencies. If you find an 95 | # order dependency and want to debug it, you can fix the order by providing 96 | # the seed, which is printed after each run. 97 | # --seed 1234 98 | config.order = :random 99 | 100 | # Seed global randomization in this process using the `--seed` CLI option. 101 | # Setting this allows you to use `--seed` to deterministically reproduce 102 | # test failures related to randomization by passing the same `--seed` value 103 | # as the one that triggered the failure. 104 | Kernel.srand config.seed 105 | =end 106 | end 107 | 108 | SERVER_URL = ENV.fetch('SERVER_URL', 'http://localhost:4242') 109 | Dotenv.load 110 | Stripe.api_key = ENV['STRIPE_SECRET_KEY'] 111 | Stripe.max_network_retries = 2 112 | Stripe.api_version = "2020-08-27" 113 | 114 | def server_url 115 | SERVER_URL 116 | end 117 | 118 | def get(path, *args, **kwargs) 119 | RestClient.get("#{SERVER_URL}#{path}", *args, **kwargs) 120 | end 121 | 122 | def get_json(path, *args, **kwargs) 123 | response = RestClient.get("#{SERVER_URL}#{path}", *args, **kwargs) 124 | JSON.parse(response.body) 125 | end 126 | 127 | def post_json(path, payload, **kwargs) 128 | defaults = {content_type: :json} 129 | response = RestClient.post( 130 | "#{SERVER_URL}#{path}", 131 | payload.to_json, 132 | defaults.merge(**kwargs) 133 | ) 134 | [JSON.parse(response.body), response.code] 135 | rescue => e 136 | begin 137 | [JSON.parse(e.http_body), e.http_code] 138 | rescue => e 139 | puts "Response:" 140 | p response 141 | throw "Failed to parse failed response" 142 | end 143 | end 144 | --------------------------------------------------------------------------------