├── .cli.json ├── .env.example ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci_dotnet.yaml │ ├── ci_e2e.yaml │ ├── ci_go.yaml │ ├── ci_java.yaml │ ├── ci_node.yaml │ ├── ci_php.yaml │ ├── ci_python.yaml │ ├── ci_ruby.yaml │ └── server_test.yaml ├── .gitignore ├── .rspec ├── .vscode └── extensions.json ├── Gemfile ├── LICENSE ├── README.md ├── fixed-price-subscriptions ├── README.md ├── client │ ├── react │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── public │ │ │ └── .gitkeep │ │ ├── src │ │ │ ├── Account.jsx │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── Cancel.jsx │ │ │ ├── Prices.jsx │ │ │ ├── Register.jsx │ │ │ ├── Subscribe.jsx │ │ │ ├── index.jsx │ │ │ └── logo.svg │ │ └── vite.config.mjs │ └── vanillajs │ │ ├── account.html │ │ ├── account.js │ │ ├── cancel.html │ │ ├── cancel.js │ │ ├── change-plan.html │ │ ├── css │ │ └── base.css │ │ ├── index.html │ │ ├── prices.html │ │ ├── prices.js │ │ ├── register.html │ │ ├── register.js │ │ ├── subscribe.html │ │ └── subscribe.js ├── demo.gif ├── package.json ├── seed.json ├── server │ ├── README.md │ ├── dotnet │ │ ├── .gitignore │ │ ├── Configuration │ │ │ └── StripeOptions.cs │ │ ├── Controllers │ │ │ └── BillingController.cs │ │ ├── Models │ │ │ ├── CancelSubscriptionRequest.cs │ │ │ ├── ConfigResponse.cs │ │ │ ├── CreateCustomerRequest.cs │ │ │ ├── CreateCustomerResponse.cs │ │ │ ├── CreateSubscriptionRequest.cs │ │ │ ├── InvoiceResponse.cs │ │ │ ├── RetrieveUpcomingInvoiceRequest.cs │ │ │ ├── SubscriptionCreateResponse.cs │ │ │ ├── SubscriptionResponse.cs │ │ │ ├── SubscriptionsResponse.cs │ │ │ └── UpdateSubscriptionRequest.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── dotnet.csproj │ │ └── dotnet.sln │ ├── go │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ └── server.go │ ├── java │ │ ├── README.md │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── stripe │ │ │ └── sample │ │ │ └── Server.java │ ├── node │ │ ├── README.md │ │ ├── package.json │ │ └── server.js │ ├── php-slim │ │ ├── .htaccess │ │ ├── README.md │ │ ├── composer.json │ │ ├── config.php │ │ └── index.php │ ├── python │ │ ├── README.md │ │ ├── requirements.txt │ │ └── server.py │ └── ruby │ │ ├── Gemfile │ │ ├── README.md │ │ ├── config_helper.rb │ │ └── server.rb └── subscription-with-fixed-price.png ├── spec ├── capybara_support.rb ├── fixed_price_e2e_spec.rb ├── fixed_price_server_spec.rb ├── per_seat_server_spec.rb ├── spec_helper.rb └── usage_based_server_spec.rb ├── usage-based-subscriptions-legacy ├── README.md ├── client │ ├── account.html │ ├── css │ │ ├── global.css │ │ ├── index.css │ │ └── normalize.css │ ├── images │ │ └── email.svg │ ├── index.html │ ├── prices.html │ └── script.js ├── package.json ├── server │ ├── README.md │ ├── dotnet │ │ ├── .gitignore │ │ ├── Configuration │ │ │ └── StripeOptions.cs │ │ ├── Controllers │ │ │ └── BillingController.cs │ │ ├── Models │ │ │ ├── CancelSubscriptionRequest.cs │ │ │ ├── ConfigResponse.cs │ │ │ ├── CreateCustomerRequest.cs │ │ │ ├── CreateCustomerResponse.cs │ │ │ ├── CreateSubscriptionRequest.cs │ │ │ ├── RetrieveCustomerPaymentMethodRequest.cs │ │ │ ├── RetrieveUpcomingInvoiceRequest.cs │ │ │ ├── RetryInvoiceRequest.cs │ │ │ └── UpdateSubscriptionRequest.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ReportUsage │ │ │ ├── Program.cs │ │ │ ├── ReportUsage.csproj │ │ │ └── ReportUsage.sln │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── dotnet.csproj │ │ └── dotnet.sln │ ├── go │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── report_usage.go │ │ └── server.go │ ├── java │ │ ├── README.md │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── stripe │ │ │ └── sample │ │ │ ├── ReportUsage.java │ │ │ └── Server.java │ ├── node │ │ ├── README.md │ │ ├── package.json │ │ ├── reportUsage.js │ │ ├── server.js │ │ └── tailwind.config.js │ ├── php │ │ ├── .htaccess │ │ ├── README.md │ │ ├── composer.json │ │ ├── config.php │ │ ├── index.php │ │ └── report_usage.php │ ├── python │ │ ├── README.md │ │ ├── report_usage.py │ │ ├── requirements.txt │ │ └── server.py │ └── ruby │ │ ├── Gemfile │ │ ├── README.md │ │ ├── report_usage.rb │ │ └── server.rb └── subscription-with-metered-usage.png └── usage-based-subscriptions ├── README.md ├── client └── react │ ├── .proxyrc │ ├── package.json │ ├── public │ └── index.html │ └── src │ ├── Api.js │ ├── App.css │ ├── App.jsx │ ├── Session.js │ ├── UsageBasedSubscriptionFlow.jsx │ ├── components │ ├── FlowContainer.css │ ├── FlowContainer.jsx │ └── StatusMessages.jsx │ ├── index.jsx │ └── steps │ ├── CollectPaymentMethodForm.jsx │ ├── CreateCustomerForm.jsx │ ├── CreateMeterEventForm.jsx │ ├── CreateMeterForm.jsx │ ├── CreatePriceForm.jsx │ └── CreateSubscriptionForm.jsx ├── package.json ├── server ├── README.md ├── dotnet │ ├── .gitignore │ ├── Configuration │ │ └── StripeOptions.cs │ ├── Controllers │ │ └── BillingController.cs │ ├── Models │ │ ├── ConfigResponse.cs │ │ ├── CreateCustomerRequest.cs │ │ ├── CreateCustomerResponse.cs │ │ ├── CreateMeterEventRequest.cs │ │ ├── CreateMeterEventResponse.cs │ │ ├── CreateMeterRequest.cs │ │ ├── CreateMeterResponse.cs │ │ ├── CreatePriceRequest.cs │ │ ├── CreatePriceResponse.cs │ │ ├── CreateSubscriptionRequest.cs │ │ ├── CreateSubscriptionResponse.cs │ │ ├── Error.cs │ │ └── Response.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── dotnet.csproj │ └── dotnet.sln ├── go │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── server.go ├── java │ ├── README.md │ ├── pom.xml │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── stripe │ │ └── sample │ │ └── Server.java ├── node │ ├── README.md │ ├── package.json │ └── server.js ├── php │ ├── .htaccess │ ├── README.md │ ├── composer.json │ ├── config.php │ └── index.php ├── python │ ├── README.md │ ├── requirements.txt │ └── server.py └── ruby │ ├── Gemfile │ ├── README.md │ └── server.rb └── ubb-demo.png /.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription-with-per-seat-prices", 3 | "configureDotEnv": true, 4 | "integrations": [ 5 | { 6 | "name": "usage-based-subscriptions", 7 | "clients": ["react"], 8 | "servers": ["java", "node", "php-slim", "python", "ruby", "go", "dotnet"] 9 | }, 10 | { 11 | "name": "fixed-price-subscriptions", 12 | "clients": ["vanillajs", "react"], 13 | "servers": ["java", "node", "php-slim", "python", "ruby", "go", "dotnet"] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Stripe keys 2 | STRIPE_PUBLISHABLE_KEY=pk_12345 3 | STRIPE_SECRET_KEY=sk_12345 4 | 5 | # Optional 6 | STRIPE_WEBHOOK_SECRET=whsec_1234 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please only file issues here that you believe represent actual bugs or feature requests for this sample. 2 | 3 | If you're having general trouble with your Stripe integration, please reach out to support using the form at https://support.stripe.com/ (preferred) or via email to support@stripe.com. 4 | 5 | If you are reporting a bug, please include the server language you're using, as well as any other details that may be helpful in reproducing the problem. 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: "/fixed-price-subscriptions/server/ruby/" 11 | schedule: 12 | interval: "weekly" 13 | day: "thursday" 14 | ignore: 15 | - dependency-name: "*" 16 | update-types: ["version-update:semver-patch"] 17 | - package-ecosystem: "bundler" 18 | directory: "/usage-based-subscriptions/server/ruby/" 19 | schedule: 20 | interval: "weekly" 21 | day: "thursday" 22 | ignore: 23 | - dependency-name: "*" 24 | update-types: ["version-update:semver-patch"] 25 | 26 | # python dependencies 27 | - package-ecosystem: "pip" 28 | directory: "/fixed-price-subscriptions/server/python/" 29 | schedule: 30 | interval: "weekly" 31 | day: "thursday" 32 | ignore: 33 | - dependency-name: "*" 34 | update-types: ["version-update:semver-patch"] 35 | - package-ecosystem: "pip" 36 | directory: "/usage-based-subscriptions/server/python/" 37 | schedule: 38 | interval: "weekly" 39 | day: "thursday" 40 | ignore: 41 | - dependency-name: "*" 42 | update-types: ["version-update:semver-patch"] 43 | 44 | # php dependencies 45 | - package-ecosystem: "composer" 46 | directory: "/fixed-price-subscriptions/server/php/" 47 | schedule: 48 | interval: "weekly" 49 | day: "thursday" 50 | ignore: 51 | - dependency-name: "*" 52 | update-types: ["version-update:semver-patch"] 53 | - package-ecosystem: "composer" 54 | directory: "/usage-based-subscriptions/server/php/" 55 | schedule: 56 | interval: "weekly" 57 | day: "thursday" 58 | ignore: 59 | - dependency-name: "*" 60 | update-types: ["version-update:semver-patch"] 61 | 62 | # node dependencies 63 | - package-ecosystem: "npm" 64 | directory: "/fixed-price-subscriptions/server/node/" 65 | schedule: 66 | interval: "weekly" 67 | day: "thursday" 68 | ignore: 69 | - dependency-name: "*" 70 | update-types: ["version-update:semver-patch"] 71 | - package-ecosystem: "npm" 72 | directory: "/fixed-price-subscriptions/server/node-typescript/" 73 | schedule: 74 | interval: "weekly" 75 | day: "thursday" 76 | ignore: 77 | - dependency-name: "*" 78 | update-types: ["version-update:semver-patch"] 79 | - package-ecosystem: "npm" 80 | directory: "/usage-based-subscriptions/server/node/" 81 | schedule: 82 | interval: "weekly" 83 | day: "thursday" 84 | ignore: 85 | - dependency-name: "*" 86 | update-types: ["version-update:semver-patch"] 87 | 88 | # go dependencies 89 | - package-ecosystem: "gomod" 90 | directory: "/fixed-price-subscriptions/server/go/" 91 | schedule: 92 | interval: "weekly" 93 | day: "thursday" 94 | ignore: 95 | - dependency-name: "*" 96 | update-types: ["version-update:semver-patch"] 97 | - package-ecosystem: "gomod" 98 | directory: "/usage-based-subscriptions/server/go/" 99 | schedule: 100 | interval: "weekly" 101 | day: "thursday" 102 | ignore: 103 | - dependency-name: "*" 104 | update-types: ["version-update:semver-patch"] 105 | 106 | # java dependencies 107 | - package-ecosystem: "maven" 108 | directory: "/fixed-price-subscriptions/server/java/" 109 | schedule: 110 | interval: "weekly" 111 | day: "thursday" 112 | ignore: 113 | - dependency-name: "*" 114 | update-types: ["version-update:semver-patch"] 115 | - package-ecosystem: "maven" 116 | directory: "/usage-based-subscriptions/server/java/" 117 | schedule: 118 | interval: "weekly" 119 | day: "thursday" 120 | ignore: 121 | - dependency-name: "*" 122 | update-types: ["version-update:semver-patch"] 123 | 124 | # dotnet dependencies 125 | - package-ecosystem: "nuget" 126 | directory: "/fixed-price-subscriptions/server/dotnet/" 127 | schedule: 128 | interval: "weekly" 129 | day: "thursday" 130 | ignore: 131 | - dependency-name: "*" 132 | update-types: ["version-update:semver-patch"] 133 | - package-ecosystem: "nuget" 134 | directory: "/usage-based-subscriptions/server/dotnet/" 135 | schedule: 136 | interval: "weekly" 137 | day: "thursday" 138 | ignore: 139 | - dependency-name: "*" 140 | update-types: ["version-update:semver-patch"] 141 | -------------------------------------------------------------------------------- /.github/workflows/ci_dotnet.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (dotnet) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/dotnet/**' 9 | - 'fixed-price-subscriptions/server/dotnet/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: dotnet 20 | server_image: mcr.microsoft.com/dotnet/sdk:6.0 21 | uses: ./.github/workflows/server_test.yaml 22 | secrets: inherit 23 | with: 24 | server_type: ${{ matrix.runtime.server_type }} 25 | server_image: ${{ matrix.runtime.server_image }} 26 | -------------------------------------------------------------------------------- /.github/workflows/ci_e2e.yaml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'fixed-price-subscriptions/client/vanillajs/**' 9 | - 'fixed-price-subscriptions/client/react/**' 10 | - 'usage-based-subscriptions/client/**' 11 | - '!**.css' 12 | - '!**.md' 13 | workflow_dispatch: 14 | 15 | env: 16 | STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} 17 | STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} 18 | PRICE: ${{ secrets.TEST_PRICE }} 19 | 20 | jobs: 21 | e2e_test: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | implementation: 27 | - client_type: vanillajs 28 | server_url: http://web:4242 29 | profile: e2e 30 | - client_type: react 31 | server_url: http://frontend:3000 32 | profile: frontend 33 | target: 34 | - sample: fixed-price-subscriptions 35 | tests: fixed_price_e2e_spec.rb 36 | env: 37 | SERVER_URL: ${{ matrix.implementation.server_url }} 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@v4 41 | 42 | - name: Checkout CI runner 43 | uses: actions/checkout@v4 44 | with: 45 | repository: 'stripe-samples/sample-ci' 46 | path: 'sample-ci' 47 | 48 | - name: Run tests 49 | run: | 50 | sed -i -E 's/http:\/\/localhost:4242/http:\/\/web:4242/' ${{ matrix.target.sample }}/client/react/vite.config.mjs 51 | ln -s react-cra sample-ci/docker/react 52 | 53 | source sample-ci/helpers.sh 54 | setup_dependencies 55 | 56 | install_docker_compose_settings 57 | export STRIPE_WEBHOOK_SECRET=$(retrieve_webhook_secret) 58 | cat <> .env 59 | DOMAIN=${{ matrix.implementation.server_url }} 60 | BASIC=${{ secrets.TEST_BASIC_PRICE }} 61 | PREMIUM=${{ secrets.TEST_PREMIUM_PRICE }} 62 | EOF 63 | 64 | configure_docker_compose_for_integration "${{ matrix.target.sample }}" node ../../client/${{ matrix.implementation.client_type }} node:lts 65 | docker compose --profile="${{ matrix.implementation.profile }}" up -d && wait_web_server 66 | command="docker compose exec -T runner bundle exec rspec spec/${{ matrix.target.tests }}" 67 | $command \ 68 | || $command --only-failures \ 69 | || $command --only-failures --format RSpec::Github::Formatter --format progress 70 | 71 | - name: Collect debug information 72 | if: ${{ failure() }} 73 | run: | 74 | cat .env 75 | cat docker-compose.yml 76 | docker compose ps -a 77 | docker compose --profile="${{ matrix.implementation.profile }}" logs web frontend 78 | 79 | docker cp $(docker compose ps -qa runner | head -1):/work/tmp . 80 | 81 | - name: Upload capybara screenshots 82 | if: ${{ failure() }} 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: screenshots 86 | path: | 87 | tmp/capybara 88 | -------------------------------------------------------------------------------- /.github/workflows/ci_go.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (Go) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/go/**' 9 | - 'fixed-price-subscriptions/server/go/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: go 20 | server_image: golang:latest 21 | - server_type: go 22 | server_image: golang:1.17 23 | uses: ./.github/workflows/server_test.yaml 24 | secrets: inherit 25 | with: 26 | server_type: ${{ matrix.runtime.server_type }} 27 | server_image: ${{ matrix.runtime.server_image }} 28 | -------------------------------------------------------------------------------- /.github/workflows/ci_java.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (java) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/java/**' 9 | - 'fixed-price-subscriptions/server/java/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: java 20 | server_image: maven:latest 21 | - server_type: java 22 | server_image: maven:3.8-openjdk-8 23 | uses: ./.github/workflows/server_test.yaml 24 | secrets: inherit 25 | with: 26 | server_type: ${{ matrix.runtime.server_type }} 27 | server_image: ${{ matrix.runtime.server_image }} 28 | -------------------------------------------------------------------------------- /.github/workflows/ci_node.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (Node) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/node/**' 9 | - 'fixed-price-subscriptions/server/node/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: node 20 | server_image: node:latest 21 | - server_type: node 22 | server_image: node:lts 23 | uses: ./.github/workflows/server_test.yaml 24 | secrets: inherit 25 | with: 26 | server_type: ${{ matrix.runtime.server_type }} 27 | server_image: ${{ matrix.runtime.server_image }} 28 | -------------------------------------------------------------------------------- /.github/workflows/ci_php.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (php-slim) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/php-slim/**' 9 | - 'fixed-price-subscriptions/server/php-slim/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: php-slim 20 | server_image: composer:2.2 21 | uses: ./.github/workflows/server_test.yaml 22 | secrets: inherit 23 | with: 24 | server_type: ${{ matrix.runtime.server_type }} 25 | server_image: ${{ matrix.runtime.server_image }} 26 | -------------------------------------------------------------------------------- /.github/workflows/ci_python.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (Python) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/python/**' 9 | - 'fixed-price-subscriptions/server/python/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: python 20 | server_image: python:latest 21 | - server_type: python 22 | server_image: python:3.8 23 | uses: ./.github/workflows/server_test.yaml 24 | secrets: inherit 25 | with: 26 | server_type: ${{ matrix.runtime.server_type }} 27 | server_image: ${{ matrix.runtime.server_image }} 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/ci_ruby.yaml: -------------------------------------------------------------------------------- 1 | name: Server Tests (Ruby) 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - fix-ci 7 | paths: 8 | - 'usage-based-subscriptions/server/ruby/**' 9 | - 'fixed-price-subscriptions/server/ruby/**' 10 | - '!**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | server_test: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | runtime: 19 | - server_type: ruby 20 | server_image: ruby:latest 21 | - server_type: ruby 22 | server_image: ruby:2.7 23 | uses: ./.github/workflows/server_test.yaml 24 | secrets: inherit 25 | with: 26 | server_type: ${{ matrix.runtime.server_type }} 27 | server_image: ${{ matrix.runtime.server_image }} 28 | -------------------------------------------------------------------------------- /.github/workflows/server_test.yaml: -------------------------------------------------------------------------------- 1 | name: Server Test 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | server_type: 7 | required: true 8 | type: string 9 | server_image: 10 | required: true 11 | type: string 12 | jobs: 13 | server_test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | target: 19 | - sample: usage-based-subscriptions 20 | tests: usage_based_server_spec.rb 21 | client_dir: ../../client 22 | - sample: fixed-price-subscriptions 23 | tests: fixed_price_server_spec.rb 24 | client_dir: ../../client/vanillajs 25 | steps: 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | 29 | - name: Checkout CI runner 30 | uses: actions/checkout@v4 31 | with: 32 | repository: 'stripe-samples/sample-ci' 33 | path: 'sample-ci' 34 | 35 | - name: Run tests 36 | env: 37 | STRIPE_PUBLISHABLE_KEY: ${{ secrets.TEST_STRIPE_PUBLISHABLE_KEY }} 38 | STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} 39 | PRICE: ${{ secrets.TEST_PRICE }} 40 | BASIC_PRICE: ${{ secrets.TEST_BASIC_PRICE }} 41 | PREMIUM_PRICE: ${{ secrets.TEST_PREMIUM_PRICE }} 42 | run: | 43 | rm -rf usage-based-subscriptions/server/dotnet/ReportUsage # causes "Program.cs(14,28): error CS0017: Program has more than one entry point defined." 44 | 45 | source sample-ci/helpers.sh 46 | setup_dependencies 47 | 48 | install_docker_compose_settings 49 | export STRIPE_WEBHOOK_SECRET=$(retrieve_webhook_secret) 50 | cat <> .env 51 | BASIC=${BASIC_PRICE} 52 | PREMIUM=${PREMIUM_PRICE} 53 | EOF 54 | 55 | configure_docker_compose_for_integration "${{ matrix.target.sample }}" "${{ inputs.server_type }}" "${{ matrix.target.client_dir }}" "${{ inputs.server_image }}" 56 | docker compose up -d && wait_web_server 57 | docker compose exec -T runner bundle exec rspec spec/${{ matrix.target.tests }} 58 | 59 | - name: Collect debug information 60 | if: ${{ failure() }} 61 | run: | 62 | cat .env 63 | cat docker-compose.yml 64 | docker compose ps -a 65 | docker compose logs web 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .vscode/* 4 | !.vscode/extensions.json 5 | .byebug_history 6 | 7 | **/Debug/** 8 | **/project-cache/** 9 | **/.vs/** 10 | **/obj/** 11 | 12 | # Dependencies 13 | node_modules 14 | package-lock.json 15 | yarn.lock 16 | !/yarn.lock 17 | composer.lock 18 | 19 | # Ruby files 20 | Gemfile.lock 21 | 22 | # Python files 23 | __pycache__ 24 | venv 25 | env 26 | 27 | # PHP files 28 | vendor 29 | logs 30 | 31 | # Java files 32 | .settings 33 | target/ 34 | .classpath 35 | .factorypath 36 | .project 37 | 38 | # Typescript 39 | dist 40 | 41 | # React 42 | build 43 | 44 | # act 45 | .secrets 46 | 47 | spec/examples.txt 48 | sample-ci/ 49 | 50 | #parcel 51 | .parcel-cache 52 | dist 53 | 54 | #idea 55 | .idea -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["stripe.vscode-stripe"] 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions using React 2 | 3 | This sample shows how to build a custom subscriptions form to take a payment 4 | using the [Subscriptions 5 | API](https://stripe.com/docs/billing/subscriptions/fixed-price), [Stripe 6 | Elements](https://stripe.com/billing/elements) and 7 | [React](https://reactjs.org/). 8 | 9 | ## Features 10 | 11 | This sample consists of a `client` in React and a `server` piece available in 7 12 | common languages. 13 | 14 | The client is implemented using `Vite` to provide the boilerplate 15 | for React. Stripe Elements is integrated using 16 | [`react-stripe-js`](https://github.com/stripe/react-stripe-js), which is the 17 | official React library provided by Stripe. 18 | 19 | ## How to run locally 20 | 21 | To run this sample locally you need to start both a local dev server for the `front-end` and another server for the `back-end`. 22 | 23 | You will need a Stripe account with its own set of [API keys](https://stripe.com/docs/development#api-keys). 24 | 25 | Follow the steps below to run locally. 26 | 27 | **1. Clone and configure the sample** 28 | 29 | The Stripe CLI is the fastest way to clone and configure a sample to run locally. 30 | 31 | **Using the Stripe CLI** 32 | 33 | 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. 34 | 35 | In your terminal shell, run the Stripe CLI command to clone the sample: 36 | 37 | ``` 38 | stripe samples create subscription-use-cases 39 | ``` 40 | 41 | 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. 42 | 43 | **Installing and cloning manually** 44 | 45 | If you do not want to use the Stripe CLI, you can manually clone and configure the sample yourself: 46 | 47 | ``` 48 | git clone https://github.com/stripe-samples/subscription-use-cases 49 | ``` 50 | 51 | Copy the .env.example file into a file named .env in the folder of the server you want to use. For example: 52 | 53 | ``` 54 | cp .env.example server/node/.env 55 | ``` 56 | 57 | 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/quickstart#api-keys) to find your API keys. 58 | 59 | ``` 60 | STRIPE_PUBLISHABLE_KEY= 61 | STRIPE_SECRET_KEY= 62 | ``` 63 | 64 | **Run react frontend client** 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 client/react/.env 70 | ``` 71 | 72 | ### Running the API server 73 | 74 | 1. Go to `/server` 75 | 1. Pick the language you are most comfortable in and follow the instructions in the directory on how to run. 76 | 77 | ### Running the React client 78 | 79 | 1. Go to `/client` 80 | 1. Run `npm install` 81 | 1. Run `npm start` and your default browser should now open with the front-end being served from `http://localhost:3000/`. 82 | 83 | ### Using the sample app 84 | 85 | When running both servers, you are now ready to use the app running in [http://localhost:3000](http://localhost:3000). 86 | 87 | 1. Enter your email address 88 | 1. Select your price 89 | 1. Enter your card details 90 | 1. 🎉 91 | 92 | ## FAQ 93 | 94 | Q: Why did you pick these frameworks? 95 | 96 | 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. 97 | 98 | ## Get support 99 | 100 | If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../../../../issues). 101 | 102 | If you have questions, comments, or need help with code, we're here to help: 103 | - on [Discord](https://stripe.com/go/developer-chat) 104 | - on Twitter at [@StripeDev](https://twitter.com/StripeDev) 105 | - on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag 106 | - by [email](mailto:support+github@stripe.com) 107 | 108 | ## Author(s) 109 | 110 | [@hideokamoto-stripe](https://twitter.com/hidetaka_dev) 111 | [@ctrudeau-stripe](https://twitter.com/trudeaucj) 112 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stripe Sample - Subscription 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixed-price-subscription", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@stripe/react-stripe-js": "^2.5.0", 7 | "@stripe/stripe-js": "^3.0.4", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0", 10 | "react-router-dom": "^6.22.1" 11 | }, 12 | "scripts": { 13 | "start": "vite", 14 | "build": "vite build" 15 | }, 16 | "browserslist": { 17 | "production": [ 18 | ">0.2%", 19 | "not dead", 20 | "not op_mini all" 21 | ], 22 | "development": [ 23 | "last 1 chrome version", 24 | "last 1 firefox version", 25 | "last 1 safari version" 26 | ] 27 | }, 28 | "devDependencies": { 29 | "@vitejs/plugin-react": "^4.2.1", 30 | "autoprefixer": "^10.4.17", 31 | "postcss-cli": "^11.0.0", 32 | "vite": "^5.1.3" 33 | }, 34 | "proxy": "http://localhost:4242" 35 | } 36 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-samples/subscription-use-cases/a948dca9e300c0a214ac141d197ddb6fe81c33e7/fixed-price-subscriptions/client/react/public/.gitkeep -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/Account.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './App.css'; 4 | 5 | const AccountSubscription = ({subscription}) => { 6 | return ( 7 |
8 |
9 |

10 | 11 | {subscription.id} 12 | 13 |

14 | 15 |

16 | Status: {subscription.status} 17 |

18 | 19 |

20 | Card last4: {subscription.default_payment_method?.card?.last4} 21 |

22 | 23 |

24 | Current period end: {(new Date(subscription.current_period_end * 1000).toString())} 25 |

26 | 27 | {/* Change plan
*/} 28 | Cancel 29 |
30 | ) 31 | } 32 | 33 | const Account = () => { 34 | const [subscriptions, setSubscriptions] = useState([]); 35 | 36 | useEffect(() => { 37 | const fetchData = async () => { 38 | const {subscriptions} = await fetch('api/subscriptions').then(r => r.json()); 39 | 40 | setSubscriptions(subscriptions.data); 41 | } 42 | fetchData(); 43 | }, []); 44 | 45 | if (!subscriptions) { 46 | return ''; 47 | } 48 | 49 | return ( 50 |
51 |

Account

52 | 53 | Add a subscription 54 | Restart demo 55 | 56 |

Subscriptions

57 | 58 |
59 | {subscriptions.map(s => { 60 | return 61 | })} 62 |
63 |
64 | ); 65 | } 66 | 67 | export default Account; 68 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway&display=swap'); 2 | 3 | :root { 4 | --light-grey: #F6F9FC; 5 | --dark-terminal-color: #0A2540; 6 | --accent-color: #635BFF; 7 | --radius: 3px; 8 | } 9 | 10 | body { 11 | padding: 20px; 12 | font-family: 'Raleway'; 13 | display: flex; 14 | justify-content: center; 15 | font-size: 1.2em; 16 | color: var(--dark-terminal-color); 17 | } 18 | 19 | form > * { 20 | margin: 10px 0; 21 | } 22 | 23 | code { 24 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 25 | monospace; 26 | } 27 | 28 | /* prices.html */ 29 | .price-list { 30 | display: flex; 31 | } 32 | 33 | .price-list > div { 34 | flex-grow: 3; 35 | margin: 0 10px; 36 | border: black solid 1px; 37 | padding: 20px; 38 | } 39 | 40 | 41 | button { 42 | background-color: var(--accent-color); 43 | } 44 | 45 | button { 46 | background: var(--accent-color); 47 | border-radius: var(--radius); 48 | color: white; 49 | border: 0; 50 | padding: 12px 16px; 51 | margin-top: 16px; 52 | font-weight: 600; 53 | cursor: pointer; 54 | transition: all 0.2s ease; 55 | display: block; 56 | } 57 | button:hover { 58 | filter: contrast(115%); 59 | } 60 | button:active { 61 | transform: translateY(0px) scale(0.98); 62 | filter: brightness(0.9); 63 | } 64 | button:disabled { 65 | opacity: 0.5; 66 | cursor: none; 67 | } 68 | 69 | input, select { 70 | display: block; 71 | font-size: 1.1em; 72 | width: 100%; 73 | } 74 | 75 | label { 76 | display: block; 77 | } 78 | 79 | a { 80 | color: var(--accent-color); 81 | font-weight: 900; 82 | } 83 | 84 | small { 85 | font-size: .6em; 86 | } 87 | 88 | fieldset, input, select { 89 | border: 1px solid #efefef; 90 | } 91 | 92 | 93 | #payment-form { 94 | border: #F6F9FC solid 1px; 95 | border-radius: var(--radius); 96 | padding: 20px; 97 | margin: 20px 0; 98 | box-shadow: 0 30px 50px -20px rgb(50 50 93 / 25%), 0 30px 60px -30px rgb(0 0 0 / 30%); 99 | } 100 | 101 | #messages { 102 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New'; 103 | background-color: #0A253C; 104 | color: #00D924; 105 | padding: 20px; 106 | margin: 20px 0; 107 | border-radius: var(--radius); 108 | font-size:0.7em; 109 | } 110 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { BrowserRouter as Switch, Route, Routes } from 'react-router-dom'; 4 | 5 | import Account from './Account'; 6 | import Cancel from './Cancel'; 7 | import Prices from './Prices'; 8 | import Register from './Register'; 9 | import Subscribe from './Subscribe'; 10 | 11 | function App(props) { 12 | return ( 13 | 14 | 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/Cancel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import { useLocation, useNavigate } from 'react-router-dom'; 4 | 5 | const Cancel = () => { 6 | const navigate = useNavigate(); 7 | const locacion = useLocation(); 8 | 9 | const handleClick = async (e) => { 10 | e.preventDefault(); 11 | 12 | await fetch('api/cancel-subscription', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify({ 18 | subscriptionId: locacion.state.subscription 19 | }), 20 | }) 21 | 22 | navigate('/account', { replace: true }); 23 | }; 24 | 25 | return ( 26 |
27 |

Cancel

28 | 29 |
30 | ) 31 | } 32 | 33 | 34 | export default Cancel; 35 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/Prices.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | 4 | const Prices = () => { 5 | const location = useLocation(); 6 | const navigate = useNavigate(); 7 | const [prices, setPrices] = useState([]); 8 | 9 | useEffect(() => { 10 | const fetchPrices = async () => { 11 | const {prices} = await fetch('api/config').then(r => r.json()); 12 | setPrices(prices); 13 | }; 14 | fetchPrices(); 15 | }, []) 16 | 17 | const createSubscription = async (priceId) => { 18 | const {subscriptionId, clientSecret} = await fetch('api/create-subscription', { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify({ 24 | priceId 25 | }), 26 | }).then(r => r.json()); 27 | 28 | navigate('/subscribe', { 29 | state: { 30 | from: location, 31 | subscriptionId, 32 | clientSecret, 33 | }, 34 | replace: false 35 | }); 36 | } 37 | 38 | 39 | return ( 40 |
41 |

Select a plan

42 | 43 |
44 | {prices.map((price) => { 45 | return ( 46 |
47 |

{price.product.name}

48 | 49 |

50 | ${price.unit_amount / 100} / month 51 |

52 | 53 | 56 |
57 | ) 58 | })} 59 |
60 |
61 | ); 62 | } 63 | 64 | export default Prices; 65 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './App.css'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const Register = () => { 6 | const [email, setEmail] = useState('jenny.rosen@example.com'); 7 | const navigate = useNavigate(); 8 | 9 | const handleSubmit = async (e) => { 10 | e.preventDefault(); 11 | await fetch('api/create-customer', { 12 | method: 'post', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | }, 16 | body: JSON.stringify({ 17 | email: email, 18 | }), 19 | }).then(r => r.json()); 20 | 21 | navigate('/prices', { replace: false }); 22 | }; 23 | 24 | return ( 25 |
26 |

Sample Photo Service

27 | 28 | picsum generated 29 | 30 |

31 | Unlimited photo hosting, and more. Cancel anytime. 32 |

33 | 34 |
35 | 44 | 45 | 48 |
49 |
50 | ); 51 | } 52 | 53 | export default Register; 54 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/Subscribe.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | CardElement, 4 | useStripe, 5 | useElements, 6 | } from '@stripe/react-stripe-js'; 7 | import { useLocation, useNavigate } from 'react-router-dom'; 8 | 9 | const Subscribe = () => { 10 | const navigate = useNavigate(); 11 | const { 12 | state: { 13 | clientSecret, 14 | } 15 | } = useLocation(); 16 | 17 | const [name, setName] = useState('Jenny Rosen'); 18 | const [messages, _setMessages] = useState(''); 19 | 20 | // helper for displaying status messages. 21 | const setMessage = (message) => { 22 | _setMessages(`${messages}\n\n${message}`); 23 | } 24 | 25 | // Initialize an instance of stripe. 26 | const stripe = useStripe(); 27 | const elements = useElements(); 28 | 29 | if (!stripe || !elements) { 30 | // Stripe.js has not loaded yet. Make sure to disable 31 | // form submission until Stripe.js has loaded. 32 | return ''; 33 | } 34 | 35 | // When the subscribe-form is submitted we do a few things: 36 | // 37 | // 1. Tokenize the payment method 38 | // 2. Create the subscription 39 | // 3. Handle any next actions like 3D Secure that are required for SCA. 40 | const handleSubmit = async (e) => { 41 | e.preventDefault(); 42 | 43 | // Get a reference to a mounted CardElement. Elements knows how 44 | // to find your CardElement because there can only ever be one of 45 | // each type of element. 46 | const cardElement = elements.getElement(CardElement); 47 | 48 | // Use card Element to tokenize payment details 49 | const { error } = await stripe.confirmCardPayment(clientSecret, { 50 | payment_method: { 51 | card: cardElement, 52 | billing_details: { 53 | name: name, 54 | } 55 | } 56 | }); 57 | 58 | if(error) { 59 | // show error and collect new card details. 60 | setMessage(error.message); 61 | return; 62 | } 63 | navigate('/account', { replace: false }); 64 | } 65 | 66 | return ( 67 | <> 68 |

Subscribe

69 | 70 |

71 | Try the successful test card: 4242424242424242. 72 |

73 | 74 |

75 | Try the test card that requires SCA: 4000002500003155. 76 |

77 | 78 |

79 | Use any future expiry date, CVC,5 digit postal code 80 |

81 | 82 |
83 | 84 |
85 | 89 | 90 | 91 | 92 | 95 | 96 |
{messages}
97 | 98 | 99 | ) 100 | } 101 | 102 | export default Subscribe; 103 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import {Elements} from '@stripe/react-stripe-js'; 5 | import {loadStripe} from '@stripe/stripe-js'; 6 | 7 | fetch('api/config') 8 | .then((response) => response.json()) 9 | .then((data) => { 10 | const stripePromise = loadStripe(data.publishableKey); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ); 20 | }) 21 | .catch((error) => { 22 | console.error('Error:', error); 23 | }); 24 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/react/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react" 2 | import { defineConfig } from "vite" 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | react(), 7 | ], 8 | server: { 9 | host: '0.0.0.0', 10 | port: 3000, 11 | proxy: { 12 | '/api': { 13 | target: 'http://localhost:4242', 14 | changeOrigin: true, 15 | rewrite: (path) => path.replace(/^\/api/, '') 16 | } 17 | } 18 | }, 19 | build: { 20 | outDir: "build", 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Account 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Account

14 | 15 | Add a subscription 16 | Restart demo 17 | 18 |

Subscriptions

19 | 20 |
21 | 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/account.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | // Fetch the list of subscriptions for this customer. 3 | const {subscriptions} = await fetch('/subscriptions').then((r) => r.json()); 4 | 5 | // Construct and display each subscription, its status, last4 of the card 6 | // used, and the current period end. 7 | const subscriptionsDiv = document.querySelector('#subscriptions'); 8 | subscriptionsDiv.innerHTML = subscriptions.data.map((subscription) => { 9 | let last4 = subscription.default_payment_method?.card?.last4 || ''; 10 | return ` 11 |
12 |

13 | 14 | ${subscription.id} 15 | 16 |

17 | 18 |

19 | Status: ${subscription.status} 20 |

21 | 22 |

23 | Card last4: ${last4} 24 |

25 | If the last4 is blank, ensure webhooks are being handled. The default payment method is set in the webhook handler. 26 | 27 |

28 | Current period end: ${new Date(subscription.current_period_end * 1000)} 29 |

30 | 31 | 33 | Cancel
34 | `; 35 | }).join('
'); 36 | }); 37 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/cancel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cancel 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Cancel

14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/cancel.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | // Fetch the ID of the subscription from the query string 3 | // params. 4 | const params = new URLSearchParams(window.location.search); 5 | const subscriptionId = params.get('subscription'); 6 | 7 | // When the cancel button is clicked, send an AJAX request 8 | // to our server to cancel the subscription. 9 | const cancelBtn = document.querySelector('#cancel-btn'); 10 | cancelBtn.addEventListener('click', async (e) => { 11 | e.preventDefault(); 12 | setMessage("Cancelling subscription..."); 13 | 14 | const {subscription} = await fetch('/cancel-subscription', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | }, 19 | body: JSON.stringify({ 20 | subscriptionId 21 | }), 22 | }) 23 | .then((response) => response.json()) 24 | 25 | // Display the status of the subscription after attempting to 26 | // cancel. 27 | setMessage(`Subscription status: ${subscription.status}`); 28 | setMessage(`Redirecting back to account in 7s.`); 29 | 30 | 31 | // Redirect to the account page. 32 | setTimeout(() => { 33 | window.location.href = "account.html"; 34 | }, 7 * 1000); 35 | }); 36 | 37 | const setMessage = (message) => { 38 | const messagesDiv = document.querySelector('#messages'); 39 | messagesDiv.innerHTML += "
" + message; 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/change-plan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Change plan 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Change plan

14 | 15 | Current, new 16 | 17 |
18 |
19 |

Basic

20 | 21 |

22 | $5.00 / month 23 |

24 | 25 | 26 | Select 27 | 28 |
29 | 30 |
31 |

Premium

32 | 33 |

34 | $15.00 / month 35 |

36 | 37 | 38 | Select 39 | 40 |
41 |
42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/css/base.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway&display=swap'); 2 | 3 | :root { 4 | --gray-offset: rgba(0, 0, 0, 0.03); 5 | --gray-border: rgba(0, 0, 0, 0.15); 6 | --gray-light: rgba(0, 0, 0, 0.4); 7 | --gray-mid: rgba(0, 0, 0, 0.7); 8 | --gray-dark: rgba(0, 0, 0, 0.9); 9 | --body-color: var(--gray-mid); 10 | --headline-color: var(--gray-dark); 11 | --accent-color: #ed5f74; 12 | --radius: 6px; 13 | } 14 | 15 | body { 16 | padding: 20px; 17 | font-family: 'Raleway'; 18 | display: flex; 19 | justify-content: center; 20 | font-size: 1.2em; 21 | } 22 | 23 | form > * { 24 | margin: 10px 0; 25 | } 26 | 27 | code { 28 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 29 | } 30 | 31 | button { 32 | background-color: #ed5f74; 33 | } 34 | 35 | button { 36 | background: var(--accent-color); 37 | border-radius: var(--radius); 38 | color: white; 39 | border: 0; 40 | padding: 12px 16px; 41 | margin-top: 16px; 42 | font-weight: 600; 43 | cursor: pointer; 44 | transition: all 0.2s ease; 45 | display: block; 46 | } 47 | button:hover { 48 | filter: contrast(115%); 49 | } 50 | button:active { 51 | transform: translateY(0px) scale(0.98); 52 | filter: brightness(0.9); 53 | } 54 | button:disabled { 55 | opacity: 0.5; 56 | cursor: none; 57 | } 58 | 59 | /* prices.html */ 60 | .price-list { 61 | display: flex; 62 | } 63 | 64 | .price-list > div { 65 | flex-grow: 3; 66 | margin: 0 10px; 67 | border: black solid 1px; 68 | padding: 20px; 69 | } 70 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Start a Subscription 6 | 7 | 8 | 9 |
10 |

Welcome to the Stripe sample for starting a new fixed price subscription.

11 | 12 |

Start Demo

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/prices.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Subscription prices 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Select a plan

13 | 14 |
15 | loading... 16 |
17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/prices.js: -------------------------------------------------------------------------------- 1 | // Fetch price data. 2 | const pricesDiv = document.querySelector('#price-list'); 3 | 4 | fetch('/config') 5 | .then((response) => response.json()) 6 | .then((data) => { 7 | pricesDiv.innerHTML = ''; 8 | if(!data.prices) { 9 | pricesDiv.innerHTML = ` 10 |

No prices found

11 | 12 |

This sample requires two prices, one with the lookup_key sample_basic and another with the lookup_key sample_premium

13 | 14 |

You can create these through the API or with the Stripe CLI using the provided seed.json fixture file with: stripe fixtures seed.json 15 | ` 16 | } 17 | 18 | data.prices.forEach((price) => { 19 | pricesDiv.innerHTML += ` 20 |

21 | 22 | ${price.unit_amount / 100} / 23 | ${price.currency} / 24 | ${price.recurring.interval} 25 | 26 | 27 |
28 | `; 29 | }); 30 | }) 31 | .catch((error) => { 32 | console.error('Error:', error); 33 | }); 34 | 35 | 36 | const createSubscription = (priceId) => { 37 | return fetch('/create-subscription', { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: JSON.stringify({ 43 | priceId: priceId, 44 | }), 45 | }) 46 | .then((response) => response.json()) 47 | .then((data) => { 48 | window.sessionStorage.setItem('subscriptionId', data.subscriptionId); 49 | window.sessionStorage.setItem('clientSecret', data.clientSecret); 50 | window.location.href = '/subscribe.html'; 51 | }) 52 | .catch((error) => { 53 | console.error('Error:', error); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Register 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Sample Photo Service

14 | stock image 15 | 16 |

17 | Unlimited photo hosting, and more. Cancel anytime. 18 |

19 | 20 |
21 | 25 | 26 | 29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/register.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', async () => { 2 | const signupForm = document.querySelector('#signup-form'); 3 | if (signupForm) { 4 | signupForm.addEventListener('submit', async (e) => { 5 | e.preventDefault(); 6 | 7 | // Grab reference to the emailInput. The email address 8 | // entered will be passed to the server and used to create 9 | // a customer. Email addresses do NOT uniquely identify 10 | // customers in Stripe. 11 | const emailInput = document.querySelector('#email'); 12 | 13 | // Create a customer. This will also set a cookie on the server 14 | // to simulate having a logged in user. 15 | const {customer} = await fetch('/create-customer', { 16 | method: 'post', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ 21 | email: emailInput.value, 22 | }), 23 | }).then(r => r.json()); 24 | 25 | // Redirect to the pricing page. 26 | window.location.href = '/prices.html'; 27 | }); 28 | } else { 29 | alert("No sign up form with ID `signup-form` found on the page."); 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/subscribe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Subscribe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Subscribe

14 | 15 |

16 | Try the successful test card: 4242424242424242. 17 |

18 | 19 |

20 | Try the test card that requires SCA: 4000002500003155. 21 |

22 | 23 |

24 | Use any future expiry date, CVC, and 5 digit postal code. 25 |

26 | 27 |
28 | 29 |
30 | 34 | 35 |
36 | 37 |
38 | 39 | 42 | 43 |
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/client/vanillajs/subscribe.js: -------------------------------------------------------------------------------- 1 | // helper method for displaying a status message. 2 | const setMessage = (message) => { 3 | const messageDiv = document.querySelector('#messages'); 4 | messageDiv.innerHTML += "
" + message; 5 | } 6 | 7 | // Fetch public key and initialize Stripe. 8 | let stripe, cardElement; 9 | 10 | fetch('/config') 11 | .then((resp) => resp.json()) 12 | .then((resp) => { 13 | stripe = Stripe(resp.publishableKey); 14 | 15 | const elements = stripe.elements(); 16 | cardElement = elements.create('card'); 17 | cardElement.mount('#card-element'); 18 | }); 19 | 20 | // Extract the client secret query string argument. This is 21 | // required to confirm the payment intent from the front-end. 22 | const subscriptionId = window.sessionStorage.getItem('subscriptionId'); 23 | const clientSecret = window.sessionStorage.getItem('clientSecret'); 24 | // This sample only supports a Subscription with payment 25 | // upfront. If you offer a trial on your subscription, then 26 | // instead of confirming the subscription's latest_invoice's 27 | // payment_intent. You'll use stripe.confirmCardSetup to confirm 28 | // the subscription's pending_setup_intent. 29 | // See https://stripe.com/docs/billing/subscriptions/trials 30 | 31 | // Payment info collection and confirmation 32 | // When the submit button is pressed, attempt to confirm the payment intent 33 | // with the information input into the card element form. 34 | // - handle payment errors by displaying an alert. The customer can update 35 | // the payment information and try again 36 | // - Stripe Elements automatically handles next actions like 3DSecure that are required for SCA 37 | // - Complete the subscription flow when the payment succeeds 38 | const form = document.querySelector('#subscribe-form'); 39 | form.addEventListener('submit', async (e) => { 40 | e.preventDefault(); 41 | const nameInput = document.getElementById('name'); 42 | 43 | // Create payment method and confirm payment intent. 44 | stripe.confirmCardPayment(clientSecret, { 45 | payment_method: { 46 | card: cardElement, 47 | billing_details: { 48 | name: nameInput.value, 49 | }, 50 | } 51 | }).then((result) => { 52 | if(result.error) { 53 | setMessage(`Payment failed: ${result.error.message}`); 54 | } else { 55 | // Redirect the customer to their account page 56 | setMessage('Success! Redirecting to your account.'); 57 | window.location.href = '/account.html'; 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-samples/subscription-use-cases/a948dca9e300c0a214ac141d197ddb6fe81c33e7/fixed-price-subscriptions/demo.gif -------------------------------------------------------------------------------- /fixed-price-subscriptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-subscription-with-fixed-price", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with fixed prices.", 5 | "main": "server.js", 6 | "scripts": { 7 | "server": "node server/node/server.js", 8 | "reactClient": "cd client/react && npm start", 9 | "startReact": "concurrently \"yarn reactClient\" \"yarn server\"", 10 | "startVanillajs": "yarn server", 11 | "start": "yarn server", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "stripe-demos", 15 | "license": "ISC", 16 | "dependencies": { 17 | "autoprefixer": "^9.7.6", 18 | "body-parser": "^1.19.0", 19 | "concurrently": "4.1.2", 20 | "cookie-parser": "^1.4.5", 21 | "dotenv": "^8.0.0", 22 | "express": "^4.17.1", 23 | "postcss-cli": "^7.1.1", 24 | "stripe": "^7.4.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/seed.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "template_version": 0 4 | }, 5 | "fixtures": [ 6 | { 7 | "name": "basic_product", 8 | "path": "/v1/products", 9 | "method": "post", 10 | "params": { 11 | "name": "Basic" 12 | } 13 | }, 14 | { 15 | "name": "basic_price", 16 | "path": "/v1/prices", 17 | "method": "post", 18 | "params": { 19 | "product": "${basic_product:id}", 20 | "lookup_key": "sample_basic", 21 | "currency": "usd", 22 | "unit_amount": 500, 23 | "recurring": { 24 | "interval": "month" 25 | }, 26 | "metadata": { 27 | "sample": "fixed-price" 28 | } 29 | } 30 | }, 31 | { 32 | "name": "premium_product", 33 | "path": "/v1/products", 34 | "method": "post", 35 | "params": { 36 | "name": "Premium" 37 | } 38 | }, 39 | { 40 | "name": "premium_price", 41 | "path": "/v1/prices", 42 | "method": "post", 43 | "params": { 44 | "product": "${premium_product:id}", 45 | "lookup_key": "sample_premium", 46 | "currency": "usd", 47 | "unit_amount": 1400, 48 | "recurring": { 49 | "interval": "month" 50 | }, 51 | "metadata": { 52 | "sample": "fixed-price" 53 | } 54 | } 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/README.md: -------------------------------------------------------------------------------- 1 | # Running the server 2 | 3 | We included several RESTful server that each implement the same endpoints and logic. 4 | Pick the language you are most comfortable with and follow the instructions in the directory on how to run. 5 | 6 | # Supported languages 7 | 8 | - [Java (Spark)](java/README.md) 9 | - [JavaScript (Node)](node/README.md) 10 | - [PHP (Slim)](php/README.md) 11 | - [Python (Flask)](python/README.md) 12 | - [Ruby (Sinatra)](ruby/README.md) 13 | - [Go](go/README.md) 14 | - [.NET](dotnet/README.md) 15 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Configuration/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | public class StripeOptions 2 | { 3 | public string PublishableKey { get; set; } 4 | public string SecretKey { get; set; } 5 | public string WebhookSecret { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/CancelSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CancelSubscriptionRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string Subscription { get; set; } 7 | } -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/ConfigResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | using System.Collections.Generic; 4 | 5 | public class ConfigResponse 6 | { 7 | [JsonProperty("publishableKey")] 8 | public string PublishableKey { get; set; } 9 | 10 | [JsonProperty("prices")] 11 | public List Prices { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/CreateCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateCustomerRequest 4 | { 5 | [JsonProperty("email")] 6 | public string Email { get; set; } 7 | } -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/CreateCustomerResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class CreateCustomerResponse 5 | { 6 | [JsonProperty("customer")] 7 | public Customer Customer { get; set; } 8 | } -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/CreateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateSubscriptionRequest 4 | { 5 | [JsonProperty("priceId")] 6 | public string PriceId { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/InvoiceResponse.cs: -------------------------------------------------------------------------------- 1 | using Stripe; 2 | using Newtonsoft.Json; 3 | 4 | public class InvoiceResponse 5 | { 6 | [JsonProperty("invoice")] 7 | public Invoice Invoice { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/RetrieveUpcomingInvoiceRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class RetrieveUpcomingInvoiceRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string Subscription { get; set; } 7 | 8 | [JsonProperty("newPriceLookupKey")] 9 | public string NewPrice { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/SubscriptionCreateResponse.cs: -------------------------------------------------------------------------------- 1 | using Stripe; 2 | using Newtonsoft.Json; 3 | 4 | 5 | public class SubscriptionCreateResponse 6 | { 7 | [JsonProperty("subscriptionId")] 8 | public string SubscriptionId { get; set; } 9 | 10 | [JsonProperty("clientSecret")] 11 | public string ClientSecret { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/SubscriptionResponse.cs: -------------------------------------------------------------------------------- 1 | using Stripe; 2 | using Newtonsoft.Json; 3 | 4 | 5 | public class SubscriptionResponse 6 | { 7 | [JsonProperty("subscription")] 8 | public Subscription Subscription { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/SubscriptionsResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class SubscriptionsResponse 5 | { 6 | [JsonProperty("subscriptions")] 7 | public StripeList Subscriptions { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Models/UpdateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class UpdateSubscriptionRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string Subscription { get; set; } 7 | 8 | [JsonProperty("newPriceLookupKey")] 9 | public string NewPrice { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace dotnet 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) 20 | { 21 | DotNetEnv.Env.Load(); 22 | return Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | webBuilder.UseWebRoot(Environment.GetEnvironmentVariable("STATIC_DIR")); 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/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 | "dotnet": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:4242", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions fixed price 2 | 3 | ## Requirements 4 | - [.NET Core](https://dotnet.microsoft.com/download) 5 | - [Configured .env file](../../../README.md#env-config) 6 | 7 | ## How to run 8 | 9 | 1. Run the application 10 | 11 | ``` 12 | dotnet run Program.cs 13 | ``` 14 | 15 | 2. Go to `http://localhost:4242` in your browser to see the demo 16 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Newtonsoft.Json.Serialization; 12 | using Stripe; 13 | 14 | namespace dotnet 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | StripeConfiguration.AppInfo = new AppInfo 29 | { 30 | Name = "stripe-samples/subscription-use-cases/fixed-price", 31 | Url = "https://github.com/stripe-samples/subscription-use-cases/fixed-price", 32 | Version = "0.0.1", 33 | }; 34 | 35 | services.Configure(options => 36 | { 37 | options.PublishableKey = Environment.GetEnvironmentVariable("STRIPE_PUBLISHABLE_KEY"); 38 | options.SecretKey = Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY"); 39 | options.WebhookSecret = Environment.GetEnvironmentVariable("STRIPE_WEBHOOK_SECRET"); 40 | }); 41 | 42 | services.AddControllersWithViews().AddNewtonsoftJson(options => 43 | { 44 | options.SerializerSettings.ContractResolver = new DefaultContractResolver 45 | { 46 | NamingStrategy = new SnakeCaseNamingStrategy(), 47 | }; 48 | }); 49 | } 50 | 51 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 52 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 53 | { 54 | if (env.IsDevelopment()) 55 | { 56 | app.UseDeveloperExceptionPage(); 57 | } 58 | else 59 | { 60 | app.UseExceptionHandler("/Error"); 61 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 62 | app.UseHsts(); 63 | } 64 | // app.UseHttpsRedirection();\ 65 | app.UseFileServer(); 66 | app.UseRouting(); 67 | app.UseAuthorization(); 68 | app.UseEndpoints(endpoints => 69 | { 70 | endpoints.MapControllerRoute( 71 | name: "default", 72 | pattern: "{controller=Home}/{action=Index}/{id?}"); 73 | }); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/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 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/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 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/dotnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/dotnet/dotnet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet", "dotnet.csproj", "{ACF37FF0-6BA0-479F-AC07-BFDB3B664025}" 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 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/go/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with fixed price 2 | 3 | A [Go](https://golang.org) implementation 4 | 5 | ## Requirements 6 | 7 | - Go 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | ## How to run 11 | 12 | 1. Run the application 13 | 14 | ``` 15 | go run server.go 16 | ``` 17 | 18 | 2. Go to `localhost:4242` to see the demo 19 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stripe-samples/subscription-use-cases/server/go 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 | gopkg.in/yaml.v2 v2.2.8 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 22 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 23 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 24 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/java/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with fixed price 2 | 3 | ## Requirements 4 | 5 | - Maven 6 | - Java 7 | - [Configured .env file](../../../README.md#env-config) 8 | 9 | 10 | 1. Build the package 11 | 12 | ``` 13 | mvn package 14 | ``` 15 | 16 | 2. Run the application 17 | 18 | ``` 19 | java -cp target/sample-jar-with-dependencies.jar com.stripe.sample.Server 20 | ``` 21 | 22 | 3. Go to `localhost:4242` in your browser to see the demo 23 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.stripe.sample 7 | subscriptions-with-fixed-price 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-simple 15 | 2.0.6 16 | 17 | 18 | com.sparkjava 19 | spark-core 20 | 2.9.4 21 | 22 | 23 | com.google.code.gson 24 | gson 25 | 2.11.0 26 | 27 | 28 | com.stripe 29 | stripe-java 30 | 27.0.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.13.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 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/node/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with fixed price 2 | 3 | An [Express server](http://expressjs.com) implementation. 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` to see the demo 26 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-with-fixed-prices", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample for collecting fixed price recurring payments with Stripe subscriptions.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "author": "stripe-demos", 10 | "license": "ISC", 11 | "dependencies": { 12 | "autoprefixer": "^10.4.0", 13 | "body-parser": "^1.19.0", 14 | "cookie-parser": "^1.4.5", 15 | "dotenv": "^16.0.0", 16 | "express": "^4.17.1", 17 | "stripe": "17.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/php-slim/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/php-slim/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with fixed price 2 | 3 | ## Requirements 4 | 5 | - PHP >= 7.1.3 6 | - Composer 7 | - [Slim](http://www.slimframework.com/) 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | composer install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | composer start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` in your browser to see the demo 26 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/php-slim/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription-use-cases/fixed-price", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with fixed prices.", 5 | "require": { 6 | "slim/slim": "^4.10", 7 | "vlucas/phpdotenv": "^3.4", 8 | "stripe/stripe-php": "^7.68.0", 9 | "monolog/monolog": "^1.17", 10 | "slim/psr7": "^1.5", 11 | "php-di/slim-bridge": "^3.2", 12 | "slim/http": "^1.2" 13 | }, 14 | "scripts": { 15 | "start": "php -S 0.0.0.0:4242 index.php" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fixed-price-subscriptions/server/php-slim/config.php: -------------------------------------------------------------------------------- 1 | div') do 16 | click_on 'Select' 17 | end 18 | 19 | fill_in :name, with: 'Jenny Rosen' 20 | 21 | within_frame find('iframe[name*=__privateStripeFrame]') do 22 | fill_in 'cardnumber', with: '4242424242424242' 23 | fill_in 'exp-date', with: '12 / 33' 24 | fill_in 'cvc', with: '123' 25 | fill_in 'postal', with: '10000' 26 | end 27 | 28 | click_on 'Subscribe' 29 | 30 | expect(page).to have_content 'Subscriptions' 31 | expect(page).to have_content 'Status: active' 32 | 33 | click_on 'Cancel' 34 | 35 | expect(page).to have_content 'Cancel' 36 | click_on 'Cancel' 37 | 38 | expect(page).to have_content 'Subscriptions' 39 | expect(page).to have_content 'Status: canceled' 40 | end 41 | 42 | example 'with the test card that requires SCA' do 43 | fill_in :email, with: 'test@example.com' 44 | click_on 'Register' 45 | 46 | within first('.price-list > div') do 47 | click_on 'Select' 48 | end 49 | 50 | fill_in :name, with: 'Jenny Rosen' 51 | 52 | within_frame find('iframe[name*=__privateStripeFrame][src*=elements]') do 53 | fill_in 'cardnumber', with: '4000002500003155' 54 | fill_in 'exp-date', with: '12 / 33' 55 | fill_in 'cvc', with: '123' 56 | fill_in 'postal', with: '10000' 57 | end 58 | 59 | click_on 'Subscribe' 60 | 61 | within_frame first('iframe[name*=__privateStripeFrame][src*=three-ds') do 62 | within_frame first('iframe#challengeFrame') do 63 | click_on 'Complete' 64 | end 65 | end 66 | 67 | expect(page).to have_content 'Subscriptions' 68 | expect(page).to have_content 'Status: active' 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/usage_based_server_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "full integration path" do 2 | it "just works" do 3 | # Get the index html page 4 | response = get("/") 5 | expect(response).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | Create a subscription for an online service with metered usage options, and work with Stripe Elements to host a payment form on your servers. 4 | This sample shows how to create a customer, set up a card for recurring use, and subscribe them to a subscription plan with 5 | [Stripe Billing](https://stripe.com/billing). 6 | 7 | 8 | **Demo** 9 | 10 | See a hosted version of the [sample](https://l2sny.sse.codesandbox.io/) in test mode or [fork on codesandbox.io](https://codesandbox.io/s/stripe-sample-usage-based-subscriptions-l2sny) 11 | 12 | The hosted demo is running in test mode -- use `4242424242424242` as a test card number with any CVC + future expiration date. 13 | 14 | Use the `4000002500003155` test card number to trigger a 3D Secure challenge flow. 15 | 16 | Read more about test cards on Stripe at https://stripe.com/docs/testing. 17 | 18 | Preview of recipe 19 | 20 | ### Features: 21 | 22 | - 💳Securely collect card details 23 | - 🔒Save the payment method details to a customer 24 | - 🚫Handle payment failures 25 | - 💰Subscribe the customer to a subscription plan 26 | - ➕Upgrade and downgrade on plans 27 | 28 | ## How to run locally 29 | 30 | This sample includes [5 server implementations](server/) in our most popular languages. Follow the steps below to run one of the servers locally. 31 | 32 | **1. Clone and configure the sample** 33 | 34 | The Stripe CLI is the fastest way to clone and configure a sample to run locally. 35 | 36 | **Using the Stripe CLI** 37 | 38 | 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. 39 | 40 | In your terminal shell, run the Stripe CLI command to clone the sample: 41 | 42 | ``` 43 | stripe samples create subscription-use-cases 44 | ``` 45 | 46 | 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. 47 | 48 | **Installing and cloning manually** 49 | 50 | ``` 51 | git clone git@github.com:stripe-samples/subscription-use-cases.git 52 | ``` 53 | 54 | Change into this directory to start configuring the sample: 55 | 56 | ``` 57 | cd subscription-uses-cases/usage-based-subscriptions 58 | ``` 59 | 60 | Copy the `.env.example` file into a file named `.env` in the folder of the server you want to use. For example: 61 | 62 | ``` 63 | cp .env.example server/node/.env 64 | ``` 65 | 66 | 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/quickstart#api-keys) to find your API keys. 67 | 68 | ``` 69 | STRIPE_PUBLISHABLE_KEY= 70 | STRIPE_SECRET_KEY= 71 | ``` 72 | 73 | `STATIC_DIR` tells the server where the client files are located and does not need to be modified unless you move the server files. 74 | 75 | **2. Create Products and Prices on Stripe** 76 | 77 | This sample requires a [Price](https://stripe.com/docs/api/prices) ID to create the subscription. Products and Prices are objects on Stripe that you use to model a subscription. 78 | 79 | You can create Products and Plans [in the Dashboard](https://dashboard.stripe.com/products) or with the [API](https://stripe.com/docs/api/prices/create). Create a Price to run this sample and add it to your `.env`. 80 | 81 | **3. 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 | ``` 86 | cd server/node # there's a README in this folder with instructions 87 | npm install 88 | npm start 89 | ``` 90 | 91 | **4. [Optional] Run a webhook locally:** 92 | 93 | You can use the Stripe CLI to forward webhook events to your server running locally. 94 | 95 | If you haven't already, [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account). 96 | 97 | ``` 98 | stripe listen --forward-to localhost:4242/webhook 99 | ``` 100 | 101 | The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your .env file. 102 | 103 | You should see events logged in the console where the CLI is running. 104 | 105 | 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). 106 | 107 | ## FAQ 108 | 109 | Q: Why did you pick these frameworks? 110 | 111 | 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. 112 | 113 | ## Get support 114 | If you found a bug or want to suggest a new [feature/use case/sample], please [file an issue](../../../issues). 115 | 116 | If you have questions, comments, or need help with code, we're here to help: 117 | - on [Discord](https://stripe.com/go/developer-chat) 118 | - on Twitter at [@StripeDev](https://twitter.com/StripeDev) 119 | - on Stack Overflow at the [stripe-payments](https://stackoverflow.com/tags/stripe-payments/info) tag 120 | - by [email](mailto:support+github@stripe.com) 121 | 122 | 123 | ## Author(s) 124 | 125 | - [@ctrudeau-stripe](https://twitter.com/trudeaucj) 126 | - [@suz-stripe](https://twitter.com/noopkat) 127 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/client/css/index.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @tailwind base; 3 | 4 | .pasha-image-stack { 5 | display: grid; 6 | grid-template-columns: auto auto; 7 | } 8 | 9 | .pasha-image-stack img { 10 | border-radius: var(--radius); 11 | background-color: var(--gray-border); 12 | box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 13 | 0 3px 6px 0 rgba(0, 0, 0, 0.07); 14 | transition: all 0.8s ease; 15 | opacity: 0; 16 | } 17 | 18 | .pasha-image-stack img:nth-child(1) { 19 | transform: translate(12px, -12px); 20 | opacity: 1; 21 | } 22 | .pasha-image-stack img:nth-child(2) { 23 | transform: translate(-24px, 16px); 24 | opacity: 1; 25 | } 26 | .pasha-image-stack img:nth-child(3) { 27 | transform: translate(68px, -100px); 28 | opacity: 1; 29 | } 30 | 31 | .loading { 32 | width: 30px; 33 | height: 30px; 34 | border: 3px solid rgba(0, 0, 0, 0.3); 35 | border-radius: 50%; 36 | border-top-color: #ed5f74; 37 | animation: spin 1s ease-in-out infinite; 38 | -webkit-animation: spin 1s ease-in-out infinite; 39 | } 40 | 41 | ::placeholder { 42 | color: 'rgba(0,0,0,0.4)'; 43 | font-size: '16px'; 44 | } 45 | 46 | @keyframes spin { 47 | to { 48 | -webkit-transform: rotate(360deg); 49 | } 50 | } 51 | @-webkit-keyframes spin { 52 | to { 53 | -webkit-transform: rotate(360deg); 54 | } 55 | } 56 | 57 | @tailwind components; 58 | /* purgecss end ignore */ 59 | @tailwind utilities; 60 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/client/images/email.svg: -------------------------------------------------------------------------------- 1 | email -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stripe Billing Subscriptions | Card and Direct Debit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 |
27 |
28 |
29 | 33 |
34 |
35 | Unlimited email sending. Cancel anytime. 36 |
37 | 38 |
39 |
40 | 46 | 53 |
54 | 55 | 63 |
64 |
65 |
66 |
69 | 73 |

74 | 75 | View 76 | 77 | Stripe sample code 78 |

79 | 82 | → 83 | 84 |
85 | 122 |
123 | 124 |
125 | 126 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-with-metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing usage based subscriptions.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server/node/server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "stripe-demos", 11 | "license": "ISC", 12 | "dependencies": { 13 | "autoprefixer": "^9.7.6", 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^8.0.0", 16 | "express": "^4.17.1", 17 | "postcss-cli": "^7.1.1", 18 | "stripe": "^7.4.0", 19 | "tailwindcss": "^1.4.5", 20 | "uuid": "^8.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/README.md: -------------------------------------------------------------------------------- 1 | # Running the server 2 | 3 | We included several RESTful server that each implement the same endpoints and logic. 4 | Pick the language you are most comfortable with and follow the instructions in the directory on how to run. 5 | 6 | # Supported languages 7 | 8 | - [Java (Spark)](java/README.md) 9 | - [JavaScript (Node)](node/README.md) 10 | - [PHP (Slim)](php/README.md) 11 | - [Python (Flask)](python/README.md) 12 | - [Ruby (Sinatra)](ruby/README.md) 13 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Configuration/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | public class StripeOptions 2 | { 3 | public string PublishableKey { get; set; } 4 | public string SecretKey { get; set; } 5 | public string WebhookSecret { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/CancelSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CancelSubscriptionRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string Subscription { get; set; } 7 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/ConfigResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class ConfigResponse 4 | { 5 | [JsonProperty("publishableKey")] 6 | public string PublishableKey { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/CreateCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateCustomerRequest 4 | { 5 | [JsonProperty("email")] 6 | public string Email { get; set; } 7 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/CreateCustomerResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class CreateCustomerResponse 5 | { 6 | [JsonProperty("customer")] 7 | public Customer Customer { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/CreateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateSubscriptionRequest 4 | { 5 | [JsonProperty("paymentMethodId")] 6 | public string PaymentMethod { get; set; } 7 | 8 | [JsonProperty("customerId")] 9 | public string Customer { get; set; } 10 | 11 | [JsonProperty("priceId")] 12 | public string Price { get; set; } 13 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/RetrieveCustomerPaymentMethodRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class RetrieveCustomerPaymentMethodRequest 4 | { 5 | [JsonProperty("paymentMethodId")] 6 | public string PaymentMethod { get; set; } 7 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/RetrieveUpcomingInvoiceRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class RetrieveUpcomingInvoiceRequest 4 | { 5 | [JsonProperty("customerId")] 6 | public string Customer { get; set; } 7 | 8 | [JsonProperty("subscriptionId")] 9 | public string Subscription { get; set; } 10 | 11 | [JsonProperty("newPriceId")] 12 | public string NewPrice { get; set; } 13 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/RetryInvoiceRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class RetryInvoiceRequest 4 | { 5 | [JsonProperty("customerId")] 6 | public string Customer { get; set; } 7 | 8 | [JsonProperty("paymentMethodId")] 9 | public string PaymentMethod { get; set; } 10 | 11 | [JsonProperty("invoiceId")] 12 | public string Invoice { get; set; } 13 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Models/UpdateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class UpdateSubscriptionRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string Subscription { get; set; } 7 | 8 | [JsonProperty("newPriceId")] 9 | public string NewPrice { get; set; } 10 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace dotnet 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) 20 | { 21 | DotNetEnv.Env.Load(); 22 | return Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | webBuilder.UseWebRoot(Environment.GetEnvironmentVariable("STATIC_DIR")); 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/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 | "dotnet": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:4242", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with useage based pricing 2 | 3 | ## Requirements 4 | - [.NET Core()]https://dotnet.microsoft.com/download) 5 | - [Configured .env file](../../../README.md#env-config) 6 | 7 | 8 | ## How to run 9 | 10 | 1. Run the application 11 | ``` 12 | dotnet run 13 | ``` 14 | 15 | 2. Go to `http://localhost:4242` in your browser to see the demo 16 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/ReportUsage/Program.cs: -------------------------------------------------------------------------------- 1 | using Stripe; 2 | using System; 3 | 4 | namespace ReportUsage 5 | { 6 | class Program 7 | { 8 | static void ReportUsage(string[] args) 9 | { 10 | 11 | // Set your secret key. Remember to switch to your live secret key in production! 12 | // See your keys here: https://dashboard.stripe.com/account/apikeys 13 | 14 | StripeConfiguration.ApiKey = "{{STRIPE_SECRET_KEY}}"; 15 | 16 | // This code can be run on an interval (e.g., every 24 hours) for each active 17 | // metered subscription. 18 | 19 | // You need to write some of your own business logic before creating the 20 | // usage record. Pull a record of a customer from your database 21 | // and extract the customer's Stripe Subscription Item ID and 22 | // usage for the day. If you aren't storing subscription item IDs, 23 | // you can retrieve the subscription and check for subscription items 24 | // https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 25 | var subscriptionItemId = "{{SUBSCRIPTION_ITEM_ID}}"; 26 | 27 | // The usage number you've been keeping track of in your database for the last 24 hours. 28 | var usageQuantity = 100; 29 | 30 | // The idempotency key allows you to retry this usage record call if it fails. 31 | var idempotencyKey = System.Guid.NewGuid().ToString(); 32 | 33 | var unixNow = (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; 34 | var timestamp = DateTimeOffset.FromUnixTimeSeconds(unixNow).UtcDateTime; 35 | 36 | var service = new UsageRecordService(); 37 | try 38 | { 39 | var usageRecord = service.Create( 40 | subscriptionItemId, 41 | new UsageRecordCreateOptions 42 | { 43 | Quantity = usageQuantity, 44 | Timestamp = timestamp, 45 | Action = "set", 46 | }, 47 | new RequestOptions 48 | { 49 | IdempotencyKey = idempotencyKey, 50 | } 51 | ); 52 | } 53 | catch (StripeException e) 54 | { 55 | Console.WriteLine($"Usage report failed for item {subscriptionItemId}:"); 56 | Console.WriteLine($"{e} (idempotency key: {idempotencyKey})"); 57 | }; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/ReportUsage/ReportUsage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/ReportUsage/ReportUsage.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReportUsage", "ReportUsage.csproj", "{A4B5A204-8BA9-4E5C-8807-F12AADC4FA8B}" 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 | {A4B5A204-8BA9-4E5C-8807-F12AADC4FA8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {A4B5A204-8BA9-4E5C-8807-F12AADC4FA8B}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {A4B5A204-8BA9-4E5C-8807-F12AADC4FA8B}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {A4B5A204-8BA9-4E5C-8807-F12AADC4FA8B}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Newtonsoft.Json.Serialization; 12 | using Stripe; 13 | 14 | namespace dotnet 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | StripeConfiguration.AppInfo = new AppInfo 29 | { 30 | Name = "stripe-samples/subscription-use-cases/usage-based-subscriptions", 31 | Url = "https://github.com/stripe-samples/subscription-use-cases/usage-based-subscriptions", 32 | Version = "0.0.1", 33 | }; 34 | 35 | services.Configure(options => 36 | { 37 | options.PublishableKey = Environment.GetEnvironmentVariable("STRIPE_PUBLISHABLE_KEY"); 38 | options.SecretKey = Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY"); 39 | options.WebhookSecret = Environment.GetEnvironmentVariable("STRIPE_WEBHOOK_SECRET"); 40 | }); 41 | 42 | services.AddControllersWithViews().AddNewtonsoftJson(options => 43 | { 44 | options.SerializerSettings.ContractResolver = new DefaultContractResolver 45 | { 46 | NamingStrategy = new SnakeCaseNamingStrategy(), 47 | }; 48 | }); 49 | } 50 | 51 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 52 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 53 | { 54 | if (env.IsDevelopment()) 55 | { 56 | app.UseDeveloperExceptionPage(); 57 | } 58 | else 59 | { 60 | app.UseExceptionHandler("/Error"); 61 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 62 | app.UseHsts(); 63 | } 64 | // app.UseHttpsRedirection();\ 65 | app.UseFileServer(); 66 | app.UseRouting(); 67 | app.UseAuthorization(); 68 | app.UseEndpoints(endpoints => 69 | { 70 | endpoints.MapControllerRoute( 71 | name: "default", 72 | pattern: "{controller=Home}/{action=Index}/{id?}"); 73 | }); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/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 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/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 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/dotnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/dotnet/dotnet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet", "dotnet.csproj", "{ACF37FF0-6BA0-479F-AC07-BFDB3B664025}" 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 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/go/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | A [Go](https://golang.org) implementation 4 | 5 | ## Requirements 6 | 7 | - Go 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Run the application 14 | 15 | ``` 16 | go run server.go 17 | ``` 18 | 19 | 2. Go to `localhost:4242` to see the demo 20 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stripe-samples/subscription-use-cases/server/go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/stripe/stripe-go v70.15.0+incompatible 8 | github.com/stripe/stripe-go/v71 v71.48.0 9 | gopkg.in/yaml.v2 v2.2.8 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/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 v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM= 11 | github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= 12 | github.com/stripe/stripe-go/v71 v71.48.0 h1:xSmbjHB1fdt6ieIf9yCGggafbzbXHPIhQj+R1gxTUHM= 13 | github.com/stripe/stripe-go/v71 v71.48.0/go.mod h1:BXYwMQe+xjYomcy5/qaTGyoyVMTP3wDCHa7DVFvg8+Y= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 16 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 24 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 25 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/go/report_usage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stripe/stripe-go" 5 | "github.com/stripe/stripe-go/usagerecord" 6 | ) 7 | 8 | func main() { 9 | // Set your secret key. Remember to switch to your live secret key in production! 10 | // See your keys here: https://dashboard.stripe.com/account/apikeys 11 | stripe.Key = os.Getenv("{{STRIPE_SECRET_KEY}}") 12 | 13 | // This code can be run on an interval (e.g., every 24 hours) for each active 14 | // metered subscription. 15 | 16 | // You need to write some of your own business logic before creating the 17 | // usage record. Pull a record of a customer from your database 18 | // and extract the customer's Stripe Subscription Item ID and 19 | // usage for the day. If you aren't storing subscription item IDs, 20 | // you can retrieve the subscription and check for subscription items 21 | // https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 22 | var subscriptionItemId = "{{SUBSCRIPTION_ITEM_ID}}" 23 | 24 | // The usage number you've been keeping track of in your database for the last 24 hours. 25 | usageQuantity := int64(100) 26 | 27 | params := &stripe.UsageRecordParams{ 28 | SubscriptionItem: stripe.String(subscriptionItemId), 29 | Quantity: stripe.Int64(usageQuantity), 30 | Timestamp: stripe.Int64(time.Now().Unix()), 31 | Action: stripe.String(string(stripe.UsageRecordActionSet)), 32 | } 33 | 34 | usagerecord.New(params) 35 | } 36 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/java/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | ## Requirements 4 | 5 | - Maven 6 | - Java 7 | - [Configured .env file](../../../README.md#env-config) 8 | 9 | 10 | 1. Build the package 11 | 12 | ``` 13 | mvn package 14 | ``` 15 | 16 | 2. Run the application 17 | 18 | ``` 19 | java -cp target/sample-jar-with-dependencies.jar com.stripe.sample.Server 20 | ``` 21 | 22 | 3. Go to `localhost:4242` in your browser to see the demo 23 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.stripe.sample 7 | subscriptions-with-metered-usage 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-simple 15 | 2.0.6 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 | 26.9.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.13.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 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/java/src/main/java/com/stripe/sample/ReportUsage.java: -------------------------------------------------------------------------------- 1 | package com.stripe.sample; 2 | 3 | import com.stripe.Stripe; 4 | import com.stripe.exception.StripeException; 5 | import com.stripe.model.UsageRecord; 6 | import com.stripe.net.RequestOptions; 7 | import com.stripe.param.UsageRecordCreateOnSubscriptionItemParams; 8 | import io.github.cdimascio.dotenv.Dotenv; 9 | // This code can be run on an interval (e.g., every 24 hours) for each active 10 | // metered subscription. 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | public class ReportUsage { 16 | 17 | public static void main(String[] args) { 18 | // Set your secret key. Remember to switch to your live secret key in production! 19 | // See your keys here: https://dashboard.stripe.com/account/apikeys 20 | Dotenv dotenv = Dotenv.load(); 21 | Stripe.apiKey = dotenv.get("STRIPE_SECRET_KEY"); 22 | 23 | // You need to write some of your own business logic before creating the 24 | // usage record. Pull a record of a customer from your database 25 | // and extract the customer's Stripe Subscription Item ID and 26 | // usage for the day. If you aren't storing subscription item IDs, 27 | // you can retrieve the subscription and check for subscription items 28 | // https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 29 | String subscriptionItemID = ""; 30 | // The usage number you've been keeping track of in your own database for the last 24 hours. 31 | long usageQuantity = 100; 32 | 33 | long timestamp = Instant.now().getEpochSecond(); 34 | // The idempotency key allows you to retry this usage record call if it fails. 35 | String idempotencyKey = UUID.randomUUID().toString(); 36 | 37 | try { 38 | UsageRecordCreateOnSubscriptionItemParams params = UsageRecordCreateOnSubscriptionItemParams 39 | .builder() 40 | .setQuantity(usageQuantity) 41 | .setTimestamp(timestamp) 42 | .setAction(UsageRecordCreateOnSubscriptionItemParams.Action.SET) 43 | .build(); 44 | 45 | RequestOptions options = RequestOptions 46 | .builder() 47 | .setIdempotencyKey(idempotencyKey) 48 | .build(); 49 | 50 | UsageRecord.createOnSubscriptionItem(subscriptionItemID, params, options); 51 | } catch (StripeException e) { 52 | System.out.println( 53 | "Usage report failed for item ID " + 54 | subscriptionItemID + 55 | " with idempotency key " + 56 | idempotencyKey + 57 | ": " + 58 | e.getMessage() 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/node/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | An [Express server](http://expressjs.com) implementation. 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` to see the demo 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-with-metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with fixed prices.", 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 | "autoprefixer": "^10.4.0", 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^16.0.0", 16 | "express": "^4.17.1", 17 | "postcss-cli": "^10.0.0", 18 | "stripe": "^16.0.0", 19 | "tailwindcss": "^3.0.7", 20 | "uuid": "^10.0.0" 21 | }, 22 | "devDependencies": { 23 | "@fullhuman/postcss-purgecss": "^6.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/node/reportUsage.js: -------------------------------------------------------------------------------- 1 | // This code can be run on an interval (e.g., every 24 hours) for each active 2 | // metered subscription. 3 | const { v4: uuid } = require('uuid'); 4 | require('dotenv').config({ path: './.env' }); 5 | // Set your secret key. Remember to switch to your live secret key in production! 6 | // See your keys here: https://dashboard.stripe.com/account/apikeys 7 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 8 | // Install uuid from npm 9 | 10 | // You need to write some of your own business logic before creating the 11 | // usage record. Pull a record of a customer from your database 12 | // and extract the customer's Stripe Subscription Item ID and 13 | // usage for the day. If you aren't storing subscription item IDs, 14 | // you can retrieve the subscription and check for subscription items 15 | // https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 16 | const subscriptionItemID = ''; 17 | // The usage number you've been keeping track of in your own database for the last 24 hours. 18 | const usageQuantity = 100; 19 | 20 | // The idempotency key allows you to retry this usage record call if it fails. 21 | const idempotencyKey = uuid(); 22 | const timestamp = parseInt(Date.now() / 1000); 23 | 24 | (async function reportUsage() { 25 | try { 26 | await stripe.subscriptionItems.createUsageRecord( 27 | subscriptionItemID, 28 | { 29 | quantity: usageQuantity, 30 | timestamp: timestamp, 31 | action: 'set', 32 | }, 33 | { 34 | idempotencyKey 35 | } 36 | ); 37 | } catch (error) { 38 | console.error(`Usage report failed for item ID ${subscriptionItemID} with idempotency key ${idempotencyKey}: ${error.toString()}`); 39 | } 40 | })(); 41 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/node/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['../../client/**/*.html'], 3 | theme: { 4 | fontFamily: { 5 | body: ['-apple-system', 'BlinkMacSystemFont', 'sans-serif'], 6 | }, 7 | extend: { 8 | colors: { 9 | pasha: '#ed5f74', 10 | }, 11 | }, 12 | }, 13 | variants: {}, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/php/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/php/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | ## Requirements 4 | 5 | - PHP >= 7.1.3 6 | - Composer 7 | - [Slim](http://www.slimframework.com/) 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | composer install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | composer start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` in your browser to see the demo 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription-use-cases/metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with fixed prices.", 5 | "require": { 6 | "slim/slim": "^4.9", 7 | "vlucas/phpdotenv": "^5.4", 8 | "stripe/stripe-php": "^15.0.0", 9 | "monolog/monolog": "^2.3", 10 | "ramsey/uuid": "^4.0" 11 | }, 12 | "scripts": { 13 | "start": "php -S 0.0.0.0:4242 index.php" 14 | } 15 | } -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/php/config.php: -------------------------------------------------------------------------------- 1 | load(); 11 | 12 | // Set your secret key. Remember to switch to your live secret key in production! 13 | // See your keys here: https://dashboard.stripe.com/account/apikeys 14 | $stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY')); 15 | 16 | // You need to write some of your own business logic before creating the 17 | // usage record. Pull a record of a customer from your database 18 | // and extract the customer's Stripe Subscription Item ID and 19 | // usage for the day. If you aren't storing subscription item IDs, 20 | // you can retrieve the subscription and check for subscription items 21 | // https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 22 | $subscription_item_id = ''; 23 | // The usage number you've been keeping track of in your database for 24 | // the last 24 hours. 25 | $usage_quantity = 100; 26 | $action = 'set'; 27 | 28 | $date = date_create(); 29 | $timestamp = date_timestamp_get($date); 30 | // The idempotency key allows you to retry this usage record call if it fails. 31 | $idempotency_key = Uuid::uuid4()->toString(); 32 | 33 | try { 34 | $stripe->subscriptionItems->createUsageRecord( 35 | $subscription_item_id, 36 | [ 37 | 'quantity' => $usage_quantity, 38 | 'timestamp' => $timestamp, 39 | 'action' => $action, 40 | ], 41 | [ 42 | 'idempotency_key' => $idempotency_key, 43 | ] 44 | ); 45 | } catch (\Stripe\Exception\ApiErrorException $error) { 46 | echo "Usage report failed for item ID $subscription_item_id with idempotency key $idempotency_key: $error.toString()"; 47 | } 48 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/python/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | ## Requirements## Requirements 4 | 5 | - Python 3 6 | - [Configured .env file](../../../README.md#env-config) 7 | 8 | 9 | ## How to run 10 | 11 | 1. Create and activate a new virtual environment 12 | 13 | **MacOS / Unix** 14 | 15 | ``` 16 | python3 -m venv env 17 | source env/bin/activate 18 | ``` 19 | 20 | **Windows (PowerShell)** 21 | 22 | ``` 23 | python3 -m venv env 24 | .\env\Scripts\activate.bat 25 | ``` 26 | 27 | 2. Install dependencies 28 | 29 | ``` 30 | pip install -r requirements.txt 31 | ``` 32 | 33 | 3. Export and run the application 34 | 35 | **MacOS / Unix** 36 | 37 | ``` 38 | export FLASK_APP=server.py 39 | python3 -m flask run --port=4242 40 | ``` 41 | 42 | **Windows (PowerShell)** 43 | 44 | ``` 45 | $env:FLASK_APP=“server.py" 46 | python3 -m flask run --port=4242 47 | ``` 48 | 49 | 4. Go to `localhost:4242` in your browser to see the demo 50 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/python/report_usage.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3.6 2 | 3 | """ 4 | report_usage.py 5 | Stripe Recipe. 6 | This code can be run on an interval (e.g., every 24 hours) for each active 7 | metered subscription. 8 | Python 3.6 or newer required. 9 | """ 10 | 11 | import os 12 | import time 13 | import uuid 14 | import stripe 15 | from dotenv import load_dotenv, find_dotenv 16 | 17 | # Setup Stripe python client library 18 | load_dotenv(find_dotenv()) 19 | stripe.api_key = os.getenv('STRIPE_SECRET_KEY') 20 | #stripe.api_version = os.getenv('STRIPE_API_VERSION') 21 | 22 | def report_usage(): 23 | # You need to write some of your own business logic before creating the 24 | # usage record. Pull a record of a customer from your database 25 | # and extract the customer's Stripe Subscription Item ID and usage 26 | # for the day. If you aren't storing subscription item IDs, 27 | # you can retrieve the subscription and check for subscription items 28 | # https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 29 | subscription_item_id = '' 30 | # The usage number you've been keeping track of in your database for 31 | # the last 24 hours. 32 | usage_quantity = 100 33 | 34 | timestamp = int(time.time()) 35 | # The idempotency key allows you to retry this usage record call if it fails. 36 | idempotency_key = str(uuid.uuid4()) 37 | 38 | try: 39 | stripe.SubscriptionItem.create_usage_record( 40 | subscription_item_id, 41 | quantity=usage_quantity, 42 | timestamp=timestamp, 43 | action='set', 44 | idempotency_key=idempotency_key 45 | ) 46 | except stripe.error.StripeError as e: 47 | print('Usage report failed for item ID %s with idempotency key %s: %s' % (subscription_item_id, idempotency_key, e)) 48 | 49 | if __name__ == '__main__': 50 | report_usage() 51 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | python-dotenv==1.0.0 3 | stripe==10.9.0 4 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org/' 4 | 5 | gem 'dotenv' 6 | gem 'json' 7 | gem 'prettier' 8 | gem 'rackup' 9 | gem 'sinatra' 10 | gem 'stripe' 11 | gem 'webrick' 12 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | A [Sinatra](http://sinatrarb.com/) implementation. 4 | 5 | ## Requirements 6 | 7 | - Ruby v2.4.5+ 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | bundle install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | ruby server.rb 23 | ``` 24 | 25 | 3. Go to `localhost:4242` in your browser to see the demo 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/server/ruby/report_usage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This code can be run on an interval (e.g., every 24 hours) for each active 3 | # metered subscription. 4 | 5 | require 'stripe' 6 | require 'dotenv' 7 | require 'securerandom' 8 | 9 | # Replace if using a different env file or config 10 | Dotenv.load 11 | # Set your secret key. Remember to switch to your live secret key in production! 12 | # See your keys here: https://dashboard.stripe.com/account/apikeys 13 | Stripe.api_key = ENV['STRIPE_SECRET_KEY'] 14 | 15 | # You need to write some of your own business logic before creating the 16 | # usage record. Pull a record of a customer from your database 17 | # and extract the customer's Stripe Subscription Item ID and usage for 18 | # the day. If you aren't storing subscription item IDs, 19 | # you can retrieve the subscription and check for subscription items 20 | # https://stripe.com/docs/api/subscriptions/object#subscription_object-items. 21 | subscription_item_id = '' 22 | # The usage number you've been keeping track of in your database for 23 | # the last 24 hours. 24 | usage_quantity = 100 25 | 26 | timestamp = Time.now.to_i 27 | # The idempotency key allows you to retry this usage record call if it fails. 28 | idempotency_key = SecureRandom.uuid 29 | 30 | begin 31 | Stripe::SubscriptionItem.create_usage_record( 32 | subscription_item_id, 33 | { quantity: usage_quantity, timestamp: timestamp, action: 'set' }, 34 | { idempotency_key: idempotency_key } 35 | ) 36 | rescue Stripe::StripeError => e 37 | puts "Usage report failed for item ID #{ 38 | subscription_item_id 39 | } with idempotency key #{idempotency_key}: #{e.error.message}" 40 | end 41 | -------------------------------------------------------------------------------- /usage-based-subscriptions-legacy/subscription-with-metered-usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-samples/subscription-use-cases/a948dca9e300c0a214ac141d197ddb6fe81c33e7/usage-based-subscriptions-legacy/subscription-with-metered-usage.png -------------------------------------------------------------------------------- /usage-based-subscriptions/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | This sample shows how to 4 | 5 | - 👤 Create a customer 6 | - 📏 Create a meter 7 | - 🏷️ Create a price with metered usage option 8 | - 💰 Create a subscription 9 | - 💳 Collect a payment method using Stripe Elements 10 | - 📄 Report usage through v2 endpoint 11 | 12 | Read more about usage-based billing at https://docs.stripe.com/billing/subscriptions/usage-based/implementation-guide 13 | 14 | Usage Based Subscription Demo 15 | 16 | ### Pre-requisite: 17 | 18 | Stripe v2 endpoint requires a [Sandbox](https://docs.stripe.com/sandboxes) secret key. Please create a sandbox account and use a Sandbox API key in this project. 19 | 20 | ## How to run locally 21 | 22 | This sample includes [5 server implementations](server/) in our most popular languages. Follow the steps below to run one of the servers locally. 23 | 24 | **1. Clone the project** 25 | 26 | ``` 27 | git clone git@github.com:stripe-samples/subscription-use-cases.git 28 | ``` 29 | 30 | Change into this directory to start configuring the sample: 31 | 32 | ``` 33 | cd subscription-uses-cases/usage-based-subscriptions 34 | ``` 35 | 36 | Copy the `.env.example` file into a file named `.env` in the folder of the server you want to use. For example: 37 | 38 | ``` 39 | cp .env.example server/node/.env 40 | ``` 41 | 42 | You will need a Stripe sandbox account in order to run the demo. Once you set up your account, go to the Stripe [developer dashboard](https://docs.stripe.com/sandboxes/dashboard/manage-access#manage-api-keys) to find your API keys. 43 | 44 | ``` 45 | STRIPE_PUBLISHABLE_KEY= 46 | STRIPE_SECRET_KEY= 47 | ``` 48 | 49 | **2. Run the server app** 50 | 51 | Pick the server language you want and follow the instructions in the server folder README on how to run. 52 | 53 | ``` 54 | cd server/node # there's a README in this folder with instructions 55 | npm install 56 | npm start 57 | ``` 58 | 59 | **3. Run the react client app** 60 | 61 | ``` 62 | cd client/react 63 | npm install 64 | npm start 65 | ``` 66 | 67 | **4. [Optional] Run a webhook locally:** 68 | 69 | You can use the Stripe CLI to forward webhook events to your server running locally. 70 | 71 | If you haven't already, [install the CLI](https://stripe.com/docs/stripe-cli) and [link your Stripe account](https://stripe.com/docs/stripe-cli#link-account). 72 | 73 | ``` 74 | stripe listen --forward-to localhost:4242/webhook 75 | ``` 76 | 77 | The CLI will print a webhook secret key to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your .env file. 78 | 79 | You should see events logged in the console where the CLI is running. 80 | 81 | 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). 82 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/.proxyrc: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://127.0.0.1:4242/", 4 | "pathRewrite": { 5 | "^/api": "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "usage-based-subscriptions", 3 | "version": "1.0.0", 4 | "description": "This sample shows how to create a usage based subscription", 5 | "scripts": { 6 | "start": "parcel ./public/index.html --open" 7 | }, 8 | "dependencies": { 9 | "@stripe/react-stripe-js": "^2.8.1", 10 | "@stripe/stripe-js": "^4.8.0", 11 | "antd": "^4.21.0", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "parcel": "^2.12.0", 17 | "process": "^0.11.10" 18 | }, 19 | "author": "seanzhang-stripe", 20 | "license": "MIT", 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Usage Based Subscription Demo 10 | 11 | 12 | 13 | 14 |
15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/Api.js: -------------------------------------------------------------------------------- 1 | const retrievePublishableKey = async () => { 2 | try { 3 | const res = await fetch('/api/config', { 4 | method: 'GET', 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | accepts: 'application/json', 8 | }, 9 | }); 10 | return await res.json(); 11 | } catch (error) { 12 | return { error }; 13 | } 14 | }; 15 | const createCustomer = async (name, email) => { 16 | try { 17 | const res = await fetch('/api/create-customer', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | accepts: 'application/json', 22 | }, 23 | body: JSON.stringify({ name, email }), 24 | }); 25 | return await res.json(); 26 | } catch (error) { 27 | return { error }; 28 | } 29 | }; 30 | 31 | const createMeter = async (displayName, eventName, aggregationFormula) => { 32 | try { 33 | const res = await fetch('/api/create-meter', { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | accepts: 'application/json', 38 | }, 39 | body: JSON.stringify({ displayName, eventName, aggregationFormula }), 40 | }); 41 | return await res.json(); 42 | } catch (error) { 43 | return { error }; 44 | } 45 | }; 46 | 47 | const createPrice = async (meterId, currency, amount, productName) => { 48 | try { 49 | const res = await fetch('/api/create-price', { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | accepts: 'application/json', 54 | }, 55 | body: JSON.stringify({ meterId, currency, amount, productName }), 56 | }); 57 | return await res.json(); 58 | } catch (error) { 59 | return { error }; 60 | } 61 | }; 62 | 63 | const createSubscription = async (customerId, priceId) => { 64 | try { 65 | const res = await fetch('/api/create-subscription', { 66 | method: 'POST', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | accepts: 'application/json', 70 | }, 71 | body: JSON.stringify({ customerId, priceId }), 72 | }); 73 | return await res.json(); 74 | } catch (error) { 75 | return { error }; 76 | } 77 | }; 78 | 79 | const createMeterEvent = async (eventName, customerId, value) => { 80 | try { 81 | const res = await fetch('/api/create-meter-event', { 82 | method: 'POST', 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | accepts: 'application/json', 86 | }, 87 | body: JSON.stringify({ eventName, customerId, value }), 88 | }); 89 | return await res.json(); 90 | } catch (error) { 91 | return { error }; 92 | } 93 | }; 94 | 95 | export { 96 | retrievePublishableKey, 97 | createCustomer, 98 | createMeter, 99 | createPrice, 100 | createSubscription, 101 | createMeterEvent, 102 | }; 103 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/App.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Raleway&display=swap'); 2 | 3 | #root { 4 | display: flex; 5 | align-items: center; 6 | } 7 | 8 | body { 9 | font-family: 'Raleway'; 10 | font-size: 16px; 11 | -webkit-font-smoothing: antialiased; 12 | margin: 0 auto; 13 | width: max-content; 14 | } 15 | 16 | form { 17 | width: 30vw; 18 | min-width: 500px; 19 | align-self: center; 20 | box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1), 21 | 0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07); 22 | border-radius: 7px; 23 | padding: 40px; 24 | } 25 | 26 | #payment-message { 27 | color: rgb(105, 115, 134); 28 | font-size: 16px; 29 | line-height: 20px; 30 | padding-top: 12px; 31 | text-align: center; 32 | } 33 | 34 | #payment-element { 35 | margin-bottom: 24px; 36 | } 37 | 38 | /* spinner/processing state, errors */ 39 | .spinner, 40 | .spinner:before, 41 | .spinner:after { 42 | border-radius: 50%; 43 | } 44 | 45 | .spinner { 46 | color: #ffffff; 47 | font-size: 22px; 48 | text-indent: -99999px; 49 | margin: 0px auto; 50 | position: relative; 51 | width: 20px; 52 | height: 20px; 53 | box-shadow: inset 0 0 0 2px; 54 | -webkit-transform: translateZ(0); 55 | -ms-transform: translateZ(0); 56 | transform: translateZ(0); 57 | } 58 | 59 | .spinner:before, 60 | .spinner:after { 61 | position: absolute; 62 | content: ''; 63 | } 64 | 65 | .spinner:before { 66 | width: 10.4px; 67 | height: 20.4px; 68 | background: #5469d4; 69 | border-radius: 20.4px 0 0 20.4px; 70 | top: -0.2px; 71 | left: -0.2px; 72 | -webkit-transform-origin: 10.4px 10.2px; 73 | transform-origin: 10.4px 10.2px; 74 | -webkit-animation: loading 2s infinite ease 1.5s; 75 | animation: loading 2s infinite ease 1.5s; 76 | } 77 | 78 | .spinner:after { 79 | width: 10.4px; 80 | height: 10.2px; 81 | background: #5469d4; 82 | border-radius: 0 10.2px 10.2px 0; 83 | top: -0.1px; 84 | left: 10.2px; 85 | -webkit-transform-origin: 0px 10.2px; 86 | transform-origin: 0px 10.2px; 87 | -webkit-animation: loading 2s infinite ease; 88 | animation: loading 2s infinite ease; 89 | } 90 | 91 | @keyframes loading { 92 | 0% { 93 | -webkit-transform: rotate(0deg); 94 | transform: rotate(0deg); 95 | } 96 | 100% { 97 | -webkit-transform: rotate(360deg); 98 | transform: rotate(360deg); 99 | } 100 | } 101 | 102 | @media only screen and (max-width: 600px) { 103 | form { 104 | width: 80vw; 105 | min-width: initial; 106 | } 107 | } 108 | 109 | #messages { 110 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New'; 111 | background-color: #0a253c; 112 | color: #00d924; 113 | padding: 20px; 114 | margin: 20px 0; 115 | border-radius: var(--radius); 116 | font-size: 0.7em; 117 | width: 1200px; 118 | max-height: 150px; 119 | overflow-y: auto; 120 | } 121 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.min.css'; 3 | import './App.css'; 4 | import SessionProvider from './Session'; 5 | import UsageBasedSubscriptionFlow from './UsageBasedSubscriptionFlow'; 6 | 7 | const App = () => { 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/Session.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, createContext } from 'react'; 2 | import { useMessages } from './components/StatusMessages'; 3 | const SessionContext = createContext(null); 4 | 5 | const SessionProvider = ({ children }) => { 6 | const [messages, addMessage] = useMessages(); 7 | // For publishable key 8 | const [publishableKey, setPublishableKey] = useState(null); 9 | 10 | // For customer creation 11 | const [name, setName] = useState(null); 12 | const [email, setEmail] = useState(null); 13 | const [customerId, setCustomerId] = useState(null); 14 | 15 | // For meter creation 16 | const [displayName, setDisplayName] = useState(null); 17 | const [eventName, setEventName] = useState(null); 18 | const [aggregationFormula, setAggregationFormula] = useState('sum'); 19 | const [meterId, setMeterId] = useState(null); 20 | 21 | // For price creation 22 | const [currency, setCurrency] = useState('usd'); 23 | const [amount, setAmount] = useState(null); 24 | const [productName, setProductName] = useState(null); 25 | const [priceId, setPriceId] = useState(null); 26 | 27 | // For subscription creation 28 | const [subscriptionId, setSubscriptionId] = useState(null); 29 | const [clientSecret, setClientSecret] = useState(null); 30 | 31 | return ( 32 | 71 | {children} 72 | 73 | ); 74 | }; 75 | 76 | const useSession = () => useContext(SessionContext); 77 | export default SessionProvider; 78 | export { useSession }; 79 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/UsageBasedSubscriptionFlow.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FlowContainer from './components/FlowContainer'; 3 | import { Typography } from 'antd'; 4 | import CreateCustomerForm from './steps/CreateCustomerForm'; 5 | import CreateMeterForm from './steps/CreateMeterForm'; 6 | import CreatePriceForm from './steps/CreatePriceForm'; 7 | import CreateSubscriptionForm from './steps/CreateSubscriptionForm'; 8 | import CreateMeterEventForm from './steps/CreateMeterEventForm'; 9 | const { Title } = Typography; 10 | 11 | import { useSession } from './Session'; 12 | import { 13 | retrievePublishableKey, 14 | createMeter, 15 | createCustomer, 16 | createPrice, 17 | createSubscription, 18 | } from './Api'; 19 | import CollectPaymentMethodForm from './steps/CollectPaymentMethodForm'; 20 | 21 | const UsageBasedSubscriptionFlow = () => { 22 | const { 23 | setPublishableKey, 24 | displayName, 25 | eventName, 26 | aggregationFormula, 27 | setMeterId, 28 | meterId, 29 | name, 30 | email, 31 | customerId, 32 | setCustomerId, 33 | currency, 34 | amount, 35 | productName, 36 | priceId, 37 | setPriceId, 38 | setSubscriptionId, 39 | setClientSecret, 40 | addMessage, 41 | messages, 42 | } = useSession(); 43 | 44 | React.useEffect(async () => { 45 | const response = await retrievePublishableKey(); 46 | const { publishableKey, error } = response; 47 | if (publishableKey) { 48 | addMessage('🔑 Retrieved publishable key'); 49 | setPublishableKey(publishableKey); 50 | } 51 | if (error) { 52 | addMessage( 53 | `😱 Failed to retrieve publisable key. Is your server running?` 54 | ); 55 | } 56 | }, []); 57 | 58 | const [currentStep, setCurrentStep] = React.useState(0); 59 | 60 | const performCreateCustomer = async () => { 61 | addMessage('🔄 Creating a Customer...'); 62 | const response = await createCustomer(name, email); 63 | const { customer, error } = response; 64 | if (customer) { 65 | addMessage(`✅ Created customer: ${customer.id}`); 66 | setCustomerId(customer.id); 67 | return true; 68 | } 69 | if (error) { 70 | addMessage(`❌ Error creating customer: ${error.message}`); 71 | return false; 72 | } 73 | }; 74 | 75 | const performCreateMeter = async () => { 76 | addMessage('🔄 Creating a Meter...'); 77 | const response = await createMeter( 78 | displayName, 79 | eventName, 80 | aggregationFormula 81 | ); 82 | const { meter, error } = response; 83 | if (meter) { 84 | addMessage(`✅ Created meter: ${meter.id}`); 85 | setMeterId(meter.id); 86 | return true; 87 | } 88 | if (error) { 89 | addMessage(`❌ Error creating meter: ${error.message}`); 90 | return false; 91 | } 92 | }; 93 | 94 | const performCreatePrice = async () => { 95 | addMessage('🔄 Creating a Price...'); 96 | const response = await createPrice(meterId, currency, amount, productName); 97 | const { price, error } = response; 98 | if (price) { 99 | addMessage(`✅ Created price: ${price.id}`); 100 | setPriceId(price.id); 101 | return true; 102 | } 103 | if (error) { 104 | addMessage(`❌ Error creating price: ${error.message}`); 105 | return false; 106 | } 107 | }; 108 | 109 | const performCreateSubscription = async () => { 110 | addMessage('🔄 Creating a Subscription...'); 111 | const response = await createSubscription(customerId, priceId); 112 | const { subscription, error } = response; 113 | if (subscription) { 114 | addMessage(`✅ Created subscription: ${subscription.id}`); 115 | setSubscriptionId(subscription.id); 116 | setClientSecret(subscription.pending_setup_intent.client_secret); 117 | return true; 118 | } 119 | if (error) { 120 | addMessage(`❌ Error creating subscription: ${error.message}`); 121 | return false; 122 | } 123 | }; 124 | 125 | const buildSteps = () => { 126 | return [ 127 | { 128 | title: 'Customer', 129 | content: , 130 | task: performCreateCustomer, 131 | }, 132 | { 133 | title: 'Meter', 134 | content: , 135 | task: performCreateMeter, 136 | }, 137 | { 138 | title: 'Price', 139 | content: , 140 | task: performCreatePrice, 141 | }, 142 | { 143 | title: 'Subscription', 144 | content: , 145 | task: performCreateSubscription, 146 | }, 147 | { 148 | title: 'Payment Method', 149 | content: , 150 | }, 151 | { 152 | title: 'Meter Event', 153 | content: , 154 | }, 155 | ]; 156 | }; 157 | 158 | return ( 159 | <> 160 | Usage Based Subscription Demo 161 | 167 | 168 | ); 169 | }; 170 | 171 | export default UsageBasedSubscriptionFlow; 172 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/components/FlowContainer.css: -------------------------------------------------------------------------------- 1 | .steps-content { 2 | min-height: 500px; 3 | width: 1200px; 4 | margin-top: 16px; 5 | padding-top: 16px; 6 | padding-left: 16px; 7 | background-color: #fafafa; 8 | border: 1px dashed #e9e9e9; 9 | border-radius: 2px; 10 | } 11 | .steps-action { 12 | margin-top: 24px; 13 | } 14 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/components/FlowContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Steps, Button, message } from 'antd'; 3 | import StatusMessages from './StatusMessages'; 4 | 5 | import './FlowContainer.css'; 6 | import 'antd/dist/antd.min.css'; 7 | 8 | const FlowContainer = ({ steps, messages, currentStep, setCurrentStep }) => { 9 | const next = async () => { 10 | const task = steps[currentStep].task; 11 | if (task) { 12 | const taskResult = await task(); 13 | if (taskResult) { 14 | setCurrentStep(currentStep + 1); 15 | } 16 | } else { 17 | setCurrentStep(currentStep + 1); 18 | } 19 | }; 20 | 21 | const prev = () => { 22 | setCurrentStep(currentStep - 1); 23 | }; 24 | 25 | return ( 26 | <> 27 | 28 |
{steps[currentStep].content}
29 |
30 | 33 | {currentStep < steps.length - 1 && ( 34 | 41 | )} 42 | {currentStep === steps.length - 1 && ( 43 | 50 | )} 51 |
52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default FlowContainer; 59 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/components/StatusMessages.jsx: -------------------------------------------------------------------------------- 1 | // A small set of helpers for displaying messages while in development. 2 | import React, { useReducer, useRef, useLayoutEffect } from 'react'; 3 | 4 | const StatusMessages = ({ messages }) => { 5 | const scrollRef = useRef(null); 6 | 7 | useLayoutEffect(() => { 8 | if (scrollRef.current) { 9 | scrollRef.current.scrollTop = scrollRef.current.scrollHeight; 10 | } 11 | }); 12 | 13 | return messages.length ? ( 14 | 19 | ) : ( 20 | '' 21 | ); 22 | }; 23 | 24 | const maybeLink = (m) => { 25 | let dashboardBase = ''; 26 | if (m.includes('cus_')) 27 | dashboardBase = 'https://dashboard.stripe.com/test/customers/'; 28 | else if (m.includes('sub_')) { 29 | dashboardBase = 'https://dashboard.stripe.com/test/subscriptions/'; 30 | } else if (m.includes('mtr_')) { 31 | dashboardBase = 'https://dashboard.stripe.com/test/meters/'; 32 | } else if (m.includes('price_')) { 33 | dashboardBase = 'https://dashboard.stripe.com/test/prices/'; 34 | } 35 | return ( 36 | $1` 41 | ), 42 | }} 43 | > 44 | ); 45 | }; 46 | 47 | const useMessages = () => { 48 | return useReducer((messages, message) => { 49 | return [...messages, message]; 50 | }, []); 51 | }; 52 | 53 | export default StatusMessages; 54 | export { useMessages }; 55 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | const container = document.getElementById('root'); 5 | const root = createRoot(container); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CollectPaymentMethodForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | PaymentElement, 4 | Elements, 5 | useStripe, 6 | useElements, 7 | } from '@stripe/react-stripe-js'; 8 | import { loadStripe } from '@stripe/stripe-js'; 9 | 10 | import { Typography, Row, Col, Button } from 'antd'; 11 | const { Title } = Typography; 12 | import { useSession } from '../Session'; 13 | 14 | const SetupForm = () => { 15 | const stripe = useStripe(); 16 | const elements = useElements(); 17 | 18 | const { addMessage } = useSession(); 19 | 20 | const performConfirmSetup = async () => { 21 | addMessage('🔄 Confirming Setup Intent...'); 22 | const { error } = await stripe.confirmSetup({ 23 | elements, 24 | confirmParams: { 25 | return_url: 'http://localhost:1234', 26 | }, 27 | redirect: 'if_required', 28 | }); 29 | if (error) { 30 | addMessage(`❌ Error confirming setup intent: ${error.message}`); 31 | } else { 32 | addMessage('✅ Confirmed Setup Intent'); 33 | } 34 | }; 35 | 36 | return ( 37 | <> 38 | Collect a Payment Method 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | const CollectPaymentMethodForm = () => { 49 | const { publishableKey, clientSecret } = useSession(); 50 | 51 | return ( 52 | 58 | 59 | 60 | ); 61 | }; 62 | export default CollectPaymentMethodForm; 63 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CreateCustomerForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Row, Col, Typography, Select } from 'antd'; 3 | const { Title } = Typography; 4 | import { useSession } from '../Session'; 5 | 6 | const CreateCustomerForm = () => { 7 | const { name, setName, email, setEmail } = useSession(); 8 | 9 | return ( 10 | <> 11 | Create a Customer 12 | 13 | 14 | Customer name 15 | 16 | setName(e.target.value)} 22 | /> 23 | 24 | 25 | 26 | Customer email 27 | 28 | setEmail(e.target.value)} 34 | /> 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default CreateCustomerForm; 41 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CreateMeterEventForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, InputNumber, Row, Col, Typography, Button } from 'antd'; 3 | const { Title } = Typography; 4 | import { useSession } from '../Session'; 5 | import { createMeterEvent } from '../Api'; 6 | 7 | const CreateMeterEventForm = () => { 8 | const { eventName, customerId, addMessage } = useSession(); 9 | const [meterEventValue, setMeterEventValue] = React.useState(0); 10 | const performCreateMeterEvent = async () => { 11 | addMessage('🔄 Creating a meter event...'); 12 | const response = await createMeterEvent( 13 | eventName, 14 | customerId, 15 | meterEventValue 16 | ); 17 | const { meterEvent, error } = response; 18 | if (meterEvent) { 19 | addMessage(`✅ Created meter event: ${meterEvent.identifier}`); 20 | return true; 21 | } 22 | if (error) { 23 | addMessage(`❌ Error creating meter event: ${error.message}`); 24 | return false; 25 | } 26 | }; 27 | 28 | return ( 29 | <> 30 | Create a Meter Event 31 | 32 | 33 | Event name 34 | 35 | 42 | 43 | 44 | 45 | 46 | Value 47 | 48 | setMeterEventValue(value)} 54 | /> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | export default CreateMeterEventForm; 66 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CreateMeterForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Row, Col, Typography, Select } from 'antd'; 3 | const { Title } = Typography; 4 | const { Option } = Select; 5 | import { useSession } from '../Session'; 6 | 7 | const CreateMeterForm = () => { 8 | const { 9 | displayName, 10 | setDisplayName, 11 | eventName, 12 | setEventName, 13 | aggregationFormula, 14 | setAggregationFormula, 15 | } = useSession(); 16 | 17 | return ( 18 | <> 19 | Create a Meter 20 | 21 | 22 | Display name 23 | 24 | setDisplayName(e.target.value)} 30 | /> 31 | 32 | 33 | 34 | Event name 35 | 36 | setEventName(e.target.value)} 42 | /> 43 | 44 | 45 | 46 | Aggregation formula 47 | 48 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default CreateMeterForm; 67 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CreatePriceForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, InputNumber, Row, Col, Typography } from 'antd'; 3 | const { Title } = Typography; 4 | 5 | import { useSession } from '../Session'; 6 | 7 | const CreatePriceForm = () => { 8 | const { 9 | currency, 10 | setCurrency, 11 | amount, 12 | setAmount, 13 | productName, 14 | setProductName, 15 | } = useSession(); 16 | 17 | return ( 18 | <> 19 | Create a Price 20 | 21 | 22 | Product name 23 | 24 | setProductName(e.target.value)} 30 | /> 31 | 32 | 33 | 34 | Currency 35 | 36 | setCurrency(e.target.value)} 42 | /> 43 | 44 | 45 | 46 | Unit amount (in cents) 47 | 48 | setAmount(value)} 54 | /> 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default CreatePriceForm; 61 | -------------------------------------------------------------------------------- /usage-based-subscriptions/client/react/src/steps/CreateSubscriptionForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Input, Row, Col, Typography } from 'antd'; 3 | const { Title } = Typography; 4 | import { useSession } from '../Session'; 5 | 6 | const CreateSubscriptionForm = () => { 7 | const { customerId, priceId } = useSession(); 8 | 9 | return ( 10 | <> 11 | Create a Subscription 12 | 13 | 14 | Customer 15 | 16 | 23 | 24 | 25 | 26 | Price 27 | 28 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default CreateSubscriptionForm; 41 | -------------------------------------------------------------------------------- /usage-based-subscriptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-with-metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing usage based subscriptions.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server/node/server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "stripe-demos", 11 | "license": "ISC", 12 | "dependencies": { 13 | "autoprefixer": "^9.7.6", 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^8.0.0", 16 | "express": "^4.17.1", 17 | "postcss-cli": "^7.1.1", 18 | "stripe": "^7.4.0", 19 | "tailwindcss": "^1.4.5", 20 | "uuid": "^8.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/README.md: -------------------------------------------------------------------------------- 1 | # Running the server 2 | 3 | We included several RESTful server that each implement the same endpoints and logic. 4 | Pick the language you are most comfortable with and follow the instructions in the directory on how to run. 5 | 6 | # Supported languages 7 | 8 | - [Java (Spark)](java/README.md) 9 | - [JavaScript (Node)](node/README.md) 10 | - [PHP (Slim)](php/README.md) 11 | - [Python (Flask)](python/README.md) 12 | - [Ruby (Sinatra)](ruby/README.md) 13 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Configuration/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | public class StripeOptions 2 | { 3 | public string PublishableKey { get; set; } 4 | public string SecretKey { get; set; } 5 | public string WebhookSecret { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/ConfigResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class ConfigResponse 4 | { 5 | [JsonProperty("publishableKey")] 6 | public string PublishableKey { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateCustomerRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateCustomerRequest 4 | { 5 | [JsonProperty("email")] 6 | public string Email { get; set; } 7 | 8 | [JsonProperty("name")] 9 | public string Name { get; set; } 10 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateCustomerResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class CreateCustomerResponse : Response 5 | { 6 | [JsonProperty("customer")] 7 | public Customer Customer { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateMeterEventRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateMeterEventRequest 4 | { 5 | [JsonProperty("eventName")] 6 | public string EventName { get; set; } 7 | 8 | [JsonProperty("customerId")] 9 | public string CustomerId { get; set; } 10 | 11 | [JsonProperty("value")] 12 | public int Value { get; set; } 13 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateMeterEventResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe.V2.Billing; 3 | 4 | public class CreateMeterEventResponse : Response 5 | { 6 | [JsonProperty("meterEvent")] 7 | public MeterEvent MeterEvent { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateMeterRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateMeterRequest 4 | { 5 | [JsonProperty("eventName")] 6 | public string EventName { get; set; } 7 | 8 | [JsonProperty("displayName")] 9 | public string DisplayName { get; set; } 10 | 11 | [JsonProperty("aggregationFormula")] 12 | public string AggregationFormula { get; set; } 13 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateMeterResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe.Billing; 3 | 4 | public class CreateMeterResponse : Response 5 | { 6 | [JsonProperty("meter")] 7 | public Meter Meter { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreatePriceRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreatePriceRequest 4 | { 5 | [JsonProperty("currency")] 6 | public string Currency { get; set; } 7 | 8 | [JsonProperty("amount")] 9 | public int Amount { get; set; } 10 | 11 | [JsonProperty("meterId")] 12 | public string MeterId { get; set; } 13 | 14 | [JsonProperty("productName")] 15 | public string ProductName { get; set; } 16 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreatePriceResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class CreatePriceResponse : Response 5 | { 6 | [JsonProperty("price")] 7 | public Price Price { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CreateSubscriptionRequest 4 | { 5 | [JsonProperty("customerId")] 6 | public string CustomerId { get; set; } 7 | 8 | [JsonProperty("priceId")] 9 | public string PriceId { get; set; } 10 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/CreateSubscriptionResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Stripe; 3 | 4 | public class CreateSubscriptionResponse : Response 5 | { 6 | [JsonProperty("subscription")] 7 | public Subscription Subscription { get; set; } 8 | } -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/Error.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class Error 4 | { 5 | [JsonProperty("message")] 6 | public string Message { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Models/Response.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class Response 4 | { 5 | [JsonProperty("error")] 6 | public Error Error {get; set;} 7 | } 8 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace dotnet 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) 20 | { 21 | DotNetEnv.Env.Load(); 22 | return Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /usage-based-subscriptions/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 | "dotnet": { 18 | "commandName": "Project", 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:4242", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with useage based pricing 2 | 3 | ## Requirements 4 | - [.NET Core()]https://dotnet.microsoft.com/download) 5 | - [Configured .env file](../../../README.md#env-config) 6 | 7 | 8 | ## How to run 9 | 10 | 1. Run the application 11 | ``` 12 | dotnet run 13 | ``` 14 | 15 | 2. Go to `http://localhost:4242` in your browser to see the demo 16 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.HttpsPolicy; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Newtonsoft.Json.Serialization; 12 | using Stripe; 13 | 14 | namespace dotnet 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | StripeConfiguration.AppInfo = new AppInfo 29 | { 30 | Name = "stripe-samples/subscription-use-cases/usage-based-subscriptions", 31 | Url = "https://github.com/stripe-samples/subscription-use-cases/usage-based-subscriptions", 32 | Version = "0.0.1", 33 | }; 34 | 35 | services.Configure(options => 36 | { 37 | options.PublishableKey = Environment.GetEnvironmentVariable("STRIPE_PUBLISHABLE_KEY"); 38 | options.SecretKey = Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY"); 39 | options.WebhookSecret = Environment.GetEnvironmentVariable("STRIPE_WEBHOOK_SECRET"); 40 | }); 41 | 42 | services.AddControllersWithViews().AddNewtonsoftJson(options => 43 | { 44 | options.SerializerSettings.ContractResolver = new DefaultContractResolver 45 | { 46 | NamingStrategy = new SnakeCaseNamingStrategy(), 47 | }; 48 | }); 49 | } 50 | 51 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 52 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 53 | { 54 | if (env.IsDevelopment()) 55 | { 56 | app.UseDeveloperExceptionPage(); 57 | } 58 | else 59 | { 60 | app.UseExceptionHandler("/Error"); 61 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 62 | app.UseHsts(); 63 | } 64 | // app.UseHttpsRedirection();\ 65 | app.UseFileServer(); 66 | app.UseRouting(); 67 | app.UseAuthorization(); 68 | app.UseEndpoints(endpoints => 69 | { 70 | endpoints.MapControllerRoute( 71 | name: "default", 72 | pattern: "{controller=Home}/{action=Index}/{id?}"); 73 | }); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /usage-based-subscriptions/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 | -------------------------------------------------------------------------------- /usage-based-subscriptions/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 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/dotnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/dotnet/dotnet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet", "dotnet.csproj", "{ACF37FF0-6BA0-479F-AC07-BFDB3B664025}" 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 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {ACF37FF0-6BA0-479F-AC07-BFDB3B664025}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/go/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | A [Go](https://golang.org) implementation 4 | 5 | ## Requirements 6 | 7 | - Go 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Run the application 14 | 15 | ``` 16 | go run server.go 17 | ``` 18 | 19 | 2. Go to `localhost:4242` to see the demo 20 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stripe-samples/subscription-use-cases/server/go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/joho/godotenv v1.5.1 7 | github.com/stripe/stripe-go/v81 v81.2.0 // indirect 8 | gopkg.in/yaml.v2 v2.2.8 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /usage-based-subscriptions/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/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM= 12 | github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY= 13 | github.com/stripe/stripe-go/v71 v71.48.0 h1:xSmbjHB1fdt6ieIf9yCGggafbzbXHPIhQj+R1gxTUHM= 14 | github.com/stripe/stripe-go/v71 v71.48.0/go.mod h1:BXYwMQe+xjYomcy5/qaTGyoyVMTP3wDCHa7DVFvg8+Y= 15 | github.com/stripe/stripe-go/v81 v81.2.0 h1:AduJoFed6xif3uG7rXRa2LxY+AJiialVA1hXDak1aUk= 16 | github.com/stripe/stripe-go/v81 v81.2.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= 19 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 20 | golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 24 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 26 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 29 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 34 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 35 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/java/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | ## Requirements 4 | 5 | - Maven 6 | - Java 7 | - [Configured .env file](../../../README.md#env-config) 8 | 9 | 10 | 1. Build the package 11 | 12 | ``` 13 | mvn package 14 | ``` 15 | 16 | 2. Run the application 17 | 18 | ``` 19 | java -cp target/sample-jar-with-dependencies.jar com.stripe.sample.Server 20 | ``` 21 | 22 | 3. Go to `localhost:4242` in your browser to see the demo 23 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.stripe.sample 7 | subscriptions-with-metered-usage 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-simple 15 | 2.0.6 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 | 28.2.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.13.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 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/node/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | An [Express server](http://expressjs.com) implementation. 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | npm install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` to see the demo 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscriptions-with-metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with metered prices.", 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 | "autoprefixer": "^10.4.0", 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^16.0.0", 16 | "express": "^4.17.1", 17 | "stripe": "^17.2.0" 18 | "postcss-cli": "^10.0.0", 19 | "tailwindcss": "^3.0.7", 20 | "uuid": "^10.0.0" 21 | }, 22 | "devDependencies": { 23 | "@fullhuman/postcss-purgecss": "^6.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/node/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | const bodyParser = require('body-parser'); 5 | // Replace if using a different env file or config 6 | require('dotenv').config({ path: './.env' }); 7 | 8 | if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_PUBLISHABLE_KEY) { 9 | console.log( 10 | 'The .env file is not configured. Follow the instructions in the readme to configure the .env file. https://github.com/stripe-samples/subscription-use-cases' 11 | ); 12 | console.log(''); 13 | process.env.STRIPE_SECRET_KEY 14 | ? '' 15 | : console.log('Add STRIPE_SECRET_KEY to your .env file.'); 16 | 17 | process.env.STRIPE_PUBLISHABLE_KEY 18 | ? '' 19 | : console.log('Add STRIPE_PUBLISHABLE_KEY to your .env file.'); 20 | 21 | process.exit(); 22 | } 23 | 24 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY, { 25 | apiVersion: '2024-09-30.acacia', 26 | appInfo: { 27 | // For sample support and debugging, not required for production: 28 | name: 'stripe-samples/subscription-use-cases/usage-based-subscriptions', 29 | version: '0.0.1', 30 | url: 'https://github.com/stripe-samples/subscription-use-cases/usage-based-subscriptions', 31 | }, 32 | }); 33 | 34 | // Use JSON parser for all non-webhook routes. 35 | app.use((req, res, next) => { 36 | if (req.originalUrl === '/webhook') { 37 | next(); 38 | } else { 39 | bodyParser.json()(req, res, next); 40 | } 41 | }); 42 | 43 | app.get('/', (req, res) => { 44 | const path = resolve(process.env.STATIC_DIR + '/index.html'); 45 | res.sendFile(path); 46 | }); 47 | 48 | app.get('/config', async (req, res) => { 49 | res.send({ 50 | publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, 51 | }); 52 | }); 53 | 54 | app.post('/create-customer', async (req, res) => { 55 | try { 56 | const customer = await stripe.customers.create({ 57 | name: req.body.name, 58 | email: req.body.email, 59 | }); 60 | res.send({ customer }); 61 | } catch (error) { 62 | res.status(400).send({ error: { message: error.message } }); 63 | } 64 | }); 65 | 66 | app.post('/create-meter', async (req, res) => { 67 | try { 68 | const meter = await stripe.billing.meters.create({ 69 | display_name: req.body.displayName, 70 | event_name: req.body.eventName, 71 | default_aggregation: { 72 | formula: req.body.aggregationFormula, 73 | }, 74 | }); 75 | res.send({ meter }); 76 | } catch (error) { 77 | res.status(400).send({ error: { message: error.message } }); 78 | } 79 | }); 80 | 81 | app.post('/create-meter-event', async (req, res) => { 82 | try { 83 | const meterEvent = await stripe.v2.billing.meterEvents.create({ 84 | event_name: req.body.eventName, 85 | payload: { 86 | value: req.body.value + '', 87 | stripe_customer_id: req.body.customerId, 88 | }, 89 | }); 90 | res.send({ meterEvent }); 91 | } catch (error) { 92 | res.status(400).send({ error: { message: error.message } }); 93 | } 94 | }); 95 | 96 | app.post('/create-price', async (req, res) => { 97 | try { 98 | const price = await stripe.prices.create({ 99 | currency: req.body.currency, 100 | unit_amount: req.body.amount, 101 | recurring: { 102 | interval: 'month', 103 | meter: req.body.meterId, 104 | usage_type: 'metered', 105 | }, 106 | product_data: { 107 | name: req.body.productName, 108 | }, 109 | }); 110 | res.send({ price }); 111 | } catch (error) { 112 | res.status(400).send({ error: { message: error.message } }); 113 | } 114 | }); 115 | 116 | app.post('/create-subscription', async (req, res) => { 117 | try { 118 | const subscription = await stripe.subscriptions.create({ 119 | customer: req.body.customerId, 120 | items: [{ price: req.body.priceId }], 121 | expand: ['pending_setup_intent'], 122 | }); 123 | res.send({ subscription }); 124 | } catch (error) { 125 | res.status(400).send({ error: { message: error.message } }); 126 | } 127 | }); 128 | 129 | app.post( 130 | '/webhook', 131 | bodyParser.raw({ type: 'application/json' }), 132 | async (req, res) => { 133 | let event; 134 | //If STRIPE_WEBHOOK_SECRET is set, verify the signature using the raw body and secret. 135 | if (process.env.STRIPE_WEBHOOK_SECRET) { 136 | try { 137 | event = stripe.webhooks.constructEvent( 138 | req.body, 139 | req.header('Stripe-Signature'), 140 | process.env.STRIPE_WEBHOOK_SECRET 141 | ); 142 | } catch (err) { 143 | console.log(err); 144 | console.log(`⚠️ Webhook signature verification failed.`); 145 | console.log( 146 | `⚠️ Check the env file and enter the correct webhook secret.` 147 | ); 148 | res.sendStatus(400); 149 | return; 150 | } 151 | } 152 | // Otherwise use the basic event deserialized with JSON.parse 153 | else { 154 | event = JSON.parse(req.body); 155 | } 156 | 157 | // Print out the event to the console 158 | console.log(`Received webhook event ${event.type} ${event.id}`); 159 | res.sendStatus(200); 160 | } 161 | ); 162 | 163 | app.listen(4242, () => console.log(`Node server listening on port ${4242}!`)); 164 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/php/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /usage-based-subscriptions/server/php/README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with metered usage 2 | 3 | ## Requirements 4 | 5 | - PHP >= 7.1.3 6 | - Composer 7 | - [Slim](http://www.slimframework.com/) 8 | - [Configured .env file](../../../README.md#env-config) 9 | 10 | 11 | ## How to run 12 | 13 | 1. Install dependencies 14 | 15 | ``` 16 | composer install 17 | ``` 18 | 19 | 2. Run the application 20 | 21 | ``` 22 | composer start 23 | ``` 24 | 25 | 3. Go to `localhost:4242` in your browser to see the demo 26 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subscription-use-cases/metered-usage", 3 | "version": "1.0.0", 4 | "description": "A Stripe sample implementing cards for subscriptions with metered prices.", 5 | "require": { 6 | "slim/slim": "^4.10", 7 | "vlucas/phpdotenv": "^3.4", 8 | "stripe/stripe-php": "^16.4.0", 9 | "monolog/monolog": "^2.3", 10 | "ramsey/uuid": "^4.0", 11 | "slim/psr7": "^1.7", 12 | "php-di/slim-bridge": "^3.2", 13 | "slim/http": "^1.4" 14 | }, 15 | "scripts": { 16 | "start": "php -S 127.0.0.1:4242 index.php" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /usage-based-subscriptions/server/php/config.php: -------------------------------------------------------------------------------- 1 |