├── .cli.json ├── .env.example ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── billing-subscriptions-quickstart.gif ├── client ├── css │ ├── global.css │ └── normalize.css ├── index.html └── script.js ├── package.json └── server ├── README.md ├── dotnet ├── .gitignore ├── Configuration │ └── StripeOptions.cs ├── Controllers │ └── SubscriptionsController.cs ├── Models │ ├── CustomerCreateRequest.cs │ ├── PublicKeyResponse.cs │ └── SubscriptionRetrieveRequest.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── README.md ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── sample.csproj ├── go ├── README.md └── server.go ├── java ├── README.md ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── stripe │ └── sample │ └── Server.java ├── node ├── README.md ├── package-lock.json ├── package.json └── server.js ├── php ├── .htaccess ├── README.md ├── composer.json ├── composer.lock ├── config.php └── index.php ├── python ├── README.md ├── requirements.txt └── server.py └── ruby ├── Gemfile ├── README.md └── server.rb /.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creating-subscriptions", 3 | "configureDotEnv": true, 4 | "integrations": [ 5 | { 6 | "name": "main", 7 | "clients": ["web"], 8 | "servers": ["java", "node", "php", "python", "ruby", "go", "dotnet"] 9 | } 10 | ] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Stripe keys 2 | STRIPE_PUBLISHABLE_KEY=pk_12345 3 | STRIPE_SECRET_KEY=sk_12345 4 | STRIPE_WEBHOOK_SECRET=whsec_1234 5 | 6 | # Billing variables 7 | SUBSCRIPTION_PRICE_ID=price_12345 8 | 9 | # Environment variables 10 | STATIC_DIR=../../client -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .vscode 4 | 5 | # Node files 6 | node_modules/ 7 | 8 | # Ruby files 9 | Gemfile.lock 10 | 11 | # Python files 12 | __pycache__ 13 | venv 14 | bin/ 15 | lib/ 16 | pyvenv.cfg 17 | 18 | # PHP files 19 | vendor 20 | logs 21 | 22 | # Java files 23 | .settings 24 | target/ 25 | .classpath 26 | .factorypath 27 | .project -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This sample has been archived and moved to [subscription-use-cases](https://github.com/stripe-samples/subscription-use-cases/) where it also contains different business models such as per seat, usage/metered, and fixed price subscriptions. 2 | -------------------------------------------------------------------------------- /billing-subscriptions-quickstart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stripe-archive/set-up-subscriptions/1ab4f1df01a401888a9e77375a966279960e4120/billing-subscriptions-quickstart.gif -------------------------------------------------------------------------------- /client/css/global.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --gray-offset: rgba(0, 0, 0, 0.03); 4 | --gray-border: rgba(0, 0, 0, 0.15); 5 | --gray-light: rgba(0, 0, 0, 0.4); 6 | --gray-mid: rgba(0, 0, 0, 0.7); 7 | --gray-dark: rgba(0, 0, 0, 0.9); 8 | --body-color: var(--gray-mid); 9 | --headline-color: var(--gray-dark); 10 | --accent-color: #0066f0; 11 | --body-font-family: -apple-system, BlinkMacSystemFont, sans-serif; 12 | --radius: 6px; 13 | --logo-image: url('https://storage.googleapis.com/stripe-sample-images/KAVHOLM.svg'); 14 | --form-width: 343px; 15 | } 16 | 17 | /* Base */ 18 | * { 19 | box-sizing: border-box; 20 | } 21 | body { 22 | font-family: var(--body-font-family); 23 | font-size: 16px; 24 | color: var(--body-color); 25 | -webkit-font-smoothing: antialiased; 26 | } 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6 { 33 | color: var(--body-color); 34 | margin-top: 2px; 35 | margin-bottom: 4px; 36 | } 37 | h1 { 38 | font-size: 27px; 39 | color: var(--headline-color); 40 | } 41 | h4 { 42 | font-weight: 500; 43 | font-size: 14px; 44 | color: var(--gray-light); 45 | } 46 | 47 | /* Layout */ 48 | .sr-root { 49 | display: flex; 50 | flex-direction: row; 51 | width: 100%; 52 | max-width: 980px; 53 | padding: 48px; 54 | align-content: center; 55 | justify-content: center; 56 | height: auto; 57 | min-height: 100vh; 58 | margin: 0 auto; 59 | } 60 | .sr-header { 61 | margin-bottom: 32px; 62 | } 63 | .sr-payment-summary { 64 | margin-top: 20px; 65 | margin-bottom: 20px; 66 | } 67 | .sr-main, 68 | .sr-content { 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: center; 72 | height: 100%; 73 | align-self: center; 74 | } 75 | .sr-main { 76 | width: var(--form-width); 77 | } 78 | .sr-content { 79 | padding-left: 48px; 80 | } 81 | .sr-header__logo { 82 | background-image: var(--logo-image); 83 | height: 24px; 84 | background-size: contain; 85 | background-repeat: no-repeat; 86 | width: 100%; 87 | } 88 | .sr-legal-text { 89 | color: var(--gray-light); 90 | text-align: center; 91 | font-size: 13px; 92 | line-height: 17px; 93 | margin-top: 12px; 94 | } 95 | .sr-field-error { 96 | color: var(--accent-color); 97 | text-align: left; 98 | font-size: 13px; 99 | line-height: 17px; 100 | margin-top: 12px; 101 | } 102 | 103 | /* Form */ 104 | .sr-form-row { 105 | margin: 16px 0; 106 | } 107 | label { 108 | font-size: 13px; 109 | font-weight: 500; 110 | margin-bottom: 8px; 111 | display: inline-block; 112 | } 113 | 114 | /* Inputs */ 115 | .sr-input, 116 | .sr-select, 117 | input[type='text'] { 118 | border: 1px solid var(--gray-border); 119 | border-radius: var(--radius); 120 | padding: 5px 12px; 121 | height: 44px; 122 | width: 100%; 123 | transition: box-shadow 0.2s ease; 124 | background: white; 125 | -moz-appearance: none; 126 | -webkit-appearance: none; 127 | appearance: none; 128 | } 129 | .sr-input:focus, 130 | input[type='text']:focus, 131 | button:focus, 132 | .focused { 133 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 134 | 0 0 0 4px rgba(50, 151, 211, 0.3); 135 | outline: none; 136 | z-index: 9; 137 | } 138 | .sr-input::placeholder, 139 | input[type='text']::placeholder { 140 | color: var(--gray-light); 141 | } 142 | 143 | /* Checkbox */ 144 | .sr-checkbox-label { 145 | position: relative; 146 | cursor: pointer; 147 | } 148 | 149 | .sr-checkbox-label input { 150 | opacity: 0; 151 | margin-right: 6px; 152 | } 153 | 154 | .sr-checkbox-label .sr-checkbox-check { 155 | position: absolute; 156 | left: 0; 157 | height: 16px; 158 | width: 16px; 159 | background-color: white; 160 | border: 1px solid var(--gray-border); 161 | border-radius: 4px; 162 | transition: all 0.2s ease; 163 | } 164 | 165 | .sr-checkbox-label input:focus ~ .sr-checkbox-check { 166 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 167 | 0 0 0 4px rgba(50, 151, 211, 0.3); 168 | outline: none; 169 | } 170 | 171 | .sr-checkbox-label input:checked ~ .sr-checkbox-check { 172 | background-color: var(--accent-color); 173 | background-image: url('https://storage.googleapis.com/stripe-sample-images/icon-checkmark.svg'); 174 | background-repeat: no-repeat; 175 | background-size: 16px; 176 | background-position: -1px -1px; 177 | } 178 | 179 | /* Select */ 180 | .sr-select { 181 | display: block; 182 | height: 44px; 183 | margin: 0; 184 | background-image: url('https://storage.googleapis.com/stripe-sample-images/icon-chevron-down.svg'); 185 | background-repeat: no-repeat, repeat; 186 | background-position: right 12px top 50%, 0 0; 187 | background-size: 0.65em auto, 100%; 188 | } 189 | .sr-select:after { 190 | } 191 | .sr-select::-ms-expand { 192 | display: none; 193 | } 194 | .sr-select:hover { 195 | cursor: pointer; 196 | } 197 | .sr-select:focus { 198 | box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07), 199 | 0 0 0 4px rgba(50, 151, 211, 0.3); 200 | outline: none; 201 | } 202 | .sr-select option { 203 | font-weight: 400; 204 | } 205 | .sr-select:invalid { 206 | color: var(--gray-light); 207 | background-opacity: 0.4; 208 | } 209 | 210 | /* Combo inputs */ 211 | .sr-combo-inputs { 212 | display: flex; 213 | flex-direction: column; 214 | } 215 | .sr-combo-inputs input, 216 | .sr-combo-inputs .sr-select { 217 | border-radius: 0; 218 | border-bottom: 0; 219 | } 220 | .sr-combo-inputs > input:first-child, 221 | .sr-combo-inputs > .sr-select:first-child { 222 | border-radius: var(--radius) var(--radius) 0 0; 223 | } 224 | .sr-combo-inputs > input:last-child, 225 | .sr-combo-inputs > .sr-select:last-child { 226 | border-radius: 0 0 var(--radius) var(--radius); 227 | border-bottom: 1px solid var(--gray-border); 228 | } 229 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:first-child { 230 | border-radius: var(--radius) 0 0 0; 231 | } 232 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:last-child { 233 | border-radius: 0 var(--radius) 0 0; 234 | } 235 | .sr-combo-inputs-row { 236 | width: 100%; 237 | display: flex; 238 | } 239 | 240 | .sr-combo-inputs-row > input { 241 | width: 100%; 242 | border-radius: 0; 243 | } 244 | 245 | .sr-combo-inputs > .sr-combo-inputs-row:first-child input:last-child { 246 | border-radius: var(--radius) var(--radius) 0 0; 247 | } 248 | 249 | .sr-combo-inputs-row:not(:first-of-type) .sr-input { 250 | border-radius: 0 0 var(--radius) var(--radius); 251 | } 252 | 253 | /* Buttons and links */ 254 | button { 255 | background: var(--accent-color); 256 | border-radius: var(--radius); 257 | color: white; 258 | border: 0; 259 | padding: 12px 16px; 260 | margin-top: 16px; 261 | font-weight: 600; 262 | cursor: pointer; 263 | transition: all 0.2s ease; 264 | display: block; 265 | } 266 | button:hover { 267 | filter: contrast(115%); 268 | } 269 | button:active { 270 | transform: translateY(0px) scale(0.98); 271 | filter: brightness(0.9); 272 | } 273 | button:disabled { 274 | opacity: 0.5; 275 | cursor: none; 276 | } 277 | 278 | .sr-payment-form button, 279 | .fullwidth { 280 | width: 100%; 281 | } 282 | 283 | a { 284 | color: var(--accent-color); 285 | text-decoration: none; 286 | transition: all 0.2s ease; 287 | } 288 | 289 | a:hover { 290 | filter: brightness(0.8); 291 | } 292 | 293 | a:active { 294 | filter: brightness(0.5); 295 | } 296 | 297 | /* Code block */ 298 | .sr-callout { 299 | background: var(--gray-offset); 300 | padding: 12px; 301 | border-radius: var(--radius); 302 | max-height: 400px; 303 | max-width: 600px; 304 | overflow: auto; 305 | } 306 | code, 307 | pre { 308 | font-family: 'SF Mono', 'IBM Plex Mono', 'Menlo', monospace; 309 | font-size: 14px; 310 | overflow-x: auto; 311 | white-space: pre-wrap; 312 | } 313 | 314 | /* Stripe Element placeholder */ 315 | .sr-card-element { 316 | padding-top: 12px; 317 | } 318 | 319 | /* Responsiveness */ 320 | @media (max-width: 720px) { 321 | .sr-root { 322 | flex-direction: column; 323 | justify-content: flex-start; 324 | padding: 48px 20px; 325 | min-width: 320px; 326 | } 327 | 328 | .sr-header__logo { 329 | background-position: center; 330 | } 331 | 332 | .sr-payment-summary { 333 | text-align: center; 334 | } 335 | 336 | .sr-content { 337 | display: none; 338 | } 339 | 340 | .sr-main { 341 | width: 100%; 342 | } 343 | } 344 | 345 | /* Pasha styles – Brand-overrides, can split these out */ 346 | :root { 347 | --accent-color: #ed5f74; 348 | --headline-color: var(--accent-color); 349 | --logo-image: url('https://storage.googleapis.com/stripe-sample-images/logo-pasha.svg'); 350 | } 351 | 352 | .pasha-image-stack { 353 | display: grid; 354 | grid-gap: 12px; 355 | grid-template-columns: auto auto; 356 | } 357 | 358 | .pasha-image-stack img { 359 | border-radius: var(--radius); 360 | background-color: var(--gray-border); 361 | box-shadow: 0 7px 14px 0 rgba(50, 50, 93, 0.1), 362 | 0 3px 6px 0 rgba(0, 0, 0, 0.07); 363 | transition: all 0.8s ease; 364 | opacity: 0; 365 | } 366 | 367 | .pasha-image-stack img:nth-child(1) { 368 | transform: translate(12px, -12px); 369 | opacity: 1; 370 | } 371 | .pasha-image-stack img:nth-child(2) { 372 | transform: translate(-24px, 16px); 373 | opacity: 1; 374 | } 375 | .pasha-image-stack img:nth-child(3) { 376 | transform: translate(68px, -100px); 377 | opacity: 1; 378 | } 379 | 380 | /* todo: spinner/processing state, errors, animations */ 381 | 382 | /* Animated form */ 383 | 384 | .sr-root { 385 | animation: 0.4s form-in; 386 | animation-fill-mode: both; 387 | animation-timing-function: ease; 388 | } 389 | 390 | .sr-payment-form .sr-form-row { 391 | animation: 0.4s field-in; 392 | animation-fill-mode: both; 393 | animation-timing-function: ease; 394 | transform-origin: 50% 0%; 395 | } 396 | 397 | /* need saas for loop :D */ 398 | .sr-payment-form .sr-form-row:nth-child(1) { 399 | animation-delay: 0; 400 | } 401 | .sr-payment-form .sr-form-row:nth-child(2) { 402 | animation-delay: 60ms; 403 | } 404 | .sr-payment-form .sr-form-row:nth-child(3) { 405 | animation-delay: 120ms; 406 | } 407 | .sr-payment-form .sr-form-row:nth-child(4) { 408 | animation-delay: 180ms; 409 | } 410 | .sr-payment-form .sr-form-row:nth-child(5) { 411 | animation-delay: 240ms; 412 | } 413 | .sr-payment-form .sr-form-row:nth-child(6) { 414 | animation-delay: 300ms; 415 | } 416 | 417 | .hidden { 418 | display: none; 419 | } 420 | 421 | .loading { 422 | display: inline-block; 423 | width: 20px; 424 | height: 20px; 425 | border: 3px solid rgba(255, 255, 255, 0.3); 426 | border-radius: 50%; 427 | border-top-color: #fff; 428 | animation: spin 1s ease-in-out infinite; 429 | -webkit-animation: spin 1s ease-in-out infinite; 430 | } 431 | 432 | @keyframes spin { 433 | to { 434 | -webkit-transform: rotate(360deg); 435 | } 436 | } 437 | @-webkit-keyframes spin { 438 | to { 439 | -webkit-transform: rotate(360deg); 440 | } 441 | } 442 | 443 | .sr-plans { 444 | display: flex; 445 | width: 600px; 446 | } 447 | 448 | .sr-plan { 449 | border: 1px solid rgb(232, 232, 232); 450 | border-radius: 6px; 451 | margin-right: 25px; 452 | height: 226px; 453 | width: 257px; 454 | } 455 | 456 | .sr-plan-inner { 457 | margin: 28px; 458 | } 459 | 460 | .sr-plan-name { 461 | color: rgb(166, 166, 166); 462 | font-size: 20px; 463 | font-weight: 500; 464 | letter-spacing: 0px; 465 | } 466 | 467 | .sr-plan-text { 468 | color: rgb(153, 153, 153); 469 | font-size: 12px; 470 | font-weight: 500; 471 | letter-spacing: 0px; 472 | line-height: 14px; 473 | } 474 | 475 | .sr-plan-button { 476 | border: 2px solid rgb(237, 95, 116); 477 | border-radius: 7px; 478 | } 479 | 480 | @keyframes field-in { 481 | 0% { 482 | opacity: 0; 483 | transform: translateY(8px) scale(0.95); 484 | } 485 | 100% { 486 | opacity: 1; 487 | transform: translateY(0px) scale(1); 488 | } 489 | } 490 | 491 | @keyframes form-in { 492 | 0% { 493 | opacity: 0; 494 | transform: scale(0.98); 495 | } 496 | 100% { 497 | opacity: 1; 498 | transform: scale(1); 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /client/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type="button"], 199 | [type="reset"], 200 | [type="submit"] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type="button"]:-moz-focusring, 222 | [type="reset"]:-moz-focusring, 223 | [type="submit"]:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type="checkbox"], 273 | [type="radio"] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type="number"]::-webkit-inner-spin-button, 283 | [type="number"]::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type="search"] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stripe Payment Page Recipe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |

$14.00

23 |

Subscribe to the 3 photo plan

24 |
25 |
26 |
27 |
28 | 31 |
32 |
33 | 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 | 51 | 55 |
56 |
57 | 63 | 69 |
70 |
71 |
72 | 77 | 82 | 87 | 92 |
93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /client/script.js: -------------------------------------------------------------------------------- 1 | var stripe; 2 | 3 | var stripeElements = function(publicKey) { 4 | stripe = Stripe(publicKey); 5 | var elements = stripe.elements(); 6 | 7 | // Element styles 8 | var style = { 9 | base: { 10 | fontSize: '16px', 11 | color: '#32325d', 12 | fontFamily: 13 | '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif', 14 | fontSmoothing: 'antialiased', 15 | '::placeholder': { 16 | color: 'rgba(0,0,0,0.4)' 17 | } 18 | } 19 | }; 20 | 21 | var card = elements.create('card', { style: style }); 22 | 23 | card.mount('#card-element'); 24 | 25 | // Element focus ring 26 | card.on('focus', function() { 27 | var el = document.getElementById('card-element'); 28 | el.classList.add('focused'); 29 | }); 30 | 31 | card.on('blur', function() { 32 | var el = document.getElementById('card-element'); 33 | el.classList.remove('focused'); 34 | }); 35 | 36 | document.querySelector('#submit').addEventListener('click', function(evt) { 37 | evt.preventDefault(); 38 | changeLoadingState(true); 39 | // Initiate payment 40 | createPaymentMethodAndCustomer(stripe, card); 41 | }); 42 | }; 43 | 44 | function showCardError(error) { 45 | changeLoadingState(false); 46 | // The card was declined (i.e. insufficient funds, card has expired, etc) 47 | var errorMsg = document.querySelector('.sr-field-error'); 48 | errorMsg.textContent = error.message; 49 | setTimeout(function() { 50 | errorMsg.textContent = ''; 51 | }, 8000); 52 | } 53 | 54 | var createPaymentMethodAndCustomer = function(stripe, card) { 55 | var cardholderEmail = document.querySelector('#email').value; 56 | stripe 57 | .createPaymentMethod({ 58 | type: 'card', 59 | card: card, 60 | billing_details: { 61 | email: cardholderEmail, 62 | }, 63 | }) 64 | .then(function(result) { 65 | if (result.error) { 66 | showCardError(result.error); 67 | } else { 68 | createCustomer(result.paymentMethod.id, cardholderEmail); 69 | } 70 | }); 71 | }; 72 | 73 | async function createCustomer(paymentMethod, cardholderEmail) { 74 | return fetch('/create-customer', { 75 | method: 'post', 76 | headers: { 77 | 'Content-Type': 'application/json' 78 | }, 79 | body: JSON.stringify({ 80 | email: cardholderEmail, 81 | payment_method: paymentMethod 82 | }) 83 | }) 84 | .then(response => { 85 | return response.json(); 86 | }) 87 | .then(subscription => { 88 | handleSubscription(subscription); 89 | }); 90 | } 91 | 92 | function handleSubscription(subscription) { 93 | const { latest_invoice } = subscription; 94 | const { payment_intent } = latest_invoice; 95 | 96 | if (payment_intent) { 97 | const { client_secret, status } = payment_intent; 98 | 99 | if (status === 'requires_action') { 100 | stripe.confirmCardPayment(client_secret).then(function(result) { 101 | if (result.error) { 102 | // Display error message in your UI. 103 | // The card was declined (i.e. insufficient funds, card has expired, etc) 104 | changeLoadingState(false); 105 | showCardError(result.error); 106 | } else { 107 | // Show a success message to your customer 108 | confirmSubscription(subscription.id); 109 | } 110 | }); 111 | } else { 112 | // No additional information was needed 113 | // Show a success message to your customer 114 | orderComplete(subscription); 115 | } 116 | } else { 117 | orderComplete(subscription); 118 | } 119 | } 120 | 121 | function confirmSubscription(subscriptionId) { 122 | return fetch('/subscription', { 123 | method: 'post', 124 | headers: { 125 | 'Content-type': 'application/json' 126 | }, 127 | body: JSON.stringify({ 128 | subscriptionId: subscriptionId 129 | }) 130 | }) 131 | .then(function(response) { 132 | return response.json(); 133 | }) 134 | .then(function(subscription) { 135 | orderComplete(subscription); 136 | }); 137 | } 138 | 139 | function getPublicKey() { 140 | return fetch('/public-key', { 141 | method: 'get', 142 | headers: { 143 | 'Content-Type': 'application/json' 144 | } 145 | }) 146 | .then(function(response) { 147 | return response.json(); 148 | }) 149 | .then(function(response) { 150 | stripeElements(response.publicKey); 151 | }); 152 | } 153 | 154 | getPublicKey(); 155 | 156 | /* ------- Post-payment helpers ------- */ 157 | 158 | /* Shows a success / error message when the payment is complete */ 159 | var orderComplete = function(subscription) { 160 | changeLoadingState(false); 161 | var subscriptionJson = JSON.stringify(subscription, null, 2); 162 | document.querySelectorAll('.payment-view').forEach(function(view) { 163 | view.classList.add('hidden'); 164 | }); 165 | document.querySelectorAll('.completed-view').forEach(function(view) { 166 | view.classList.remove('hidden'); 167 | }); 168 | document.querySelector('.order-status').textContent = subscription.status; 169 | document.querySelector('code').textContent = subscriptionJson; 170 | }; 171 | 172 | // Show a spinner on subscription submission 173 | var changeLoadingState = function(isLoading) { 174 | if (isLoading) { 175 | document.querySelector('#spinner').classList.add('loading'); 176 | document.querySelector('button').disabled = true; 177 | 178 | document.querySelector('#button-text').classList.add('hidden'); 179 | } else { 180 | document.querySelector('button').disabled = false; 181 | document.querySelector('#spinner').classList.remove('loading'); 182 | document.querySelector('#button-text').classList.remove('hidden'); 183 | } 184 | }; 185 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-billing-card-subscription-quickstart", 3 | "version": "1.0.0", 4 | "description": "A Stripe Billing demo to accept cards to create subscriptions", 5 | "main": "index.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 | "body-parser": "^1.19.0", 14 | "dotenv": "^8.0.0", 15 | "express": "^4.17.1", 16 | "stripe": "^7.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 in and follow the instructions in the directory on how to run. 5 | 6 | # Supported languages 7 | 8 | * [JavaScript (Node)](node/README.md) 9 | * [Python (Flask)](python/README.md) 10 | * [Ruby (Sinatra)](ruby/README.md) 11 | * [PHP (Slim)](php/README.md) 12 | * [Java (Spark)](java/README.md) 13 | * [Go](go/README.md) 14 | * [C# (ASP.Net)](dotnet/README.md) -------------------------------------------------------------------------------- /server/dotnet/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ -------------------------------------------------------------------------------- /server/dotnet/Configuration/StripeOptions.cs: -------------------------------------------------------------------------------- 1 | public class StripeOptions 2 | { 3 | public string StripePublishableKey { get; set; } 4 | public string StripeSecretKey { get; set; } 5 | public string StripeWebhookSecret { get; set; } 6 | public string SubscriptionPriceId { get; set; } 7 | } -------------------------------------------------------------------------------- /server/dotnet/Controllers/SubscriptionsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using Newtonsoft.Json; 9 | using Stripe; 10 | 11 | public class SubscriptionsController : Controller 12 | { 13 | 14 | private readonly StripeClient client; 15 | private readonly IOptions options; 16 | private readonly ILogger logger; 17 | 18 | public SubscriptionsController(IOptions options, ILogger logger) 19 | { 20 | this.options = options; 21 | this.client = new StripeClient(options.Value.StripeSecretKey); 22 | this.logger = logger; 23 | } 24 | 25 | [HttpGet("public-key")] 26 | public ActionResult GetPublishableKey() 27 | { 28 | return new PublicKeyResponse 29 | { 30 | PublicKey = this.options.Value.StripePublishableKey, 31 | }; 32 | } 33 | 34 | [HttpPost("create-customer")] 35 | public async Task> CreateCustomerAsync([FromBody] CustomerCreateRequest request) 36 | { 37 | var customerService = new CustomerService(this.client); 38 | 39 | var customer = await customerService.CreateAsync(new CustomerCreateOptions 40 | { 41 | Email = request.Email, 42 | PaymentMethod = request.PaymentMethod, 43 | InvoiceSettings = new CustomerInvoiceSettingsOptions 44 | { 45 | DefaultPaymentMethod = request.PaymentMethod, 46 | } 47 | }); 48 | 49 | var subscriptionService = new SubscriptionService(this.client); 50 | 51 | var subscription = await subscriptionService.CreateAsync(new SubscriptionCreateOptions 52 | { 53 | Items = new List 54 | { 55 | new SubscriptionItemOptions 56 | { 57 | Price = this.options.Value.SubscriptionPriceId, 58 | }, 59 | }, 60 | Customer = customer.Id, 61 | Expand = new List 62 | { 63 | "latest_invoice.payment_intent", 64 | } 65 | }); 66 | 67 | return subscription; 68 | } 69 | 70 | [HttpPost("subscription")] 71 | public async Task> RetrieveSubscriptionAsync([FromBody] SubscriptionRetrieveRequest request) 72 | { 73 | var subscriptionService = new SubscriptionService(this.client); 74 | 75 | return await subscriptionService.GetAsync(request.SubscriptionId); 76 | } 77 | 78 | [HttpPost("webhook")] 79 | public async Task ProcessWebhookEvent() 80 | { 81 | var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); 82 | 83 | try 84 | { 85 | var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], this.options.Value.StripeWebhookSecret); 86 | logger.LogInformation($"Webhook event type: {stripeEvent.Type}"); 87 | logger.LogInformation(json); 88 | return Ok(); 89 | } 90 | catch (Exception e) 91 | { 92 | logger.LogError(e, "Exception while processing webhook event."); 93 | return BadRequest(); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /server/dotnet/Models/CustomerCreateRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class CustomerCreateRequest 4 | { 5 | 6 | [JsonProperty("email")] 7 | public string Email { get; set; } 8 | 9 | [JsonProperty("payment_method")] 10 | public string PaymentMethod { get; set; } 11 | } -------------------------------------------------------------------------------- /server/dotnet/Models/PublicKeyResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class PublicKeyResponse 4 | { 5 | [JsonProperty("publicKey")] 6 | public string PublicKey { get; set; } 7 | } -------------------------------------------------------------------------------- /server/dotnet/Models/SubscriptionRetrieveRequest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | public class SubscriptionRetrieveRequest 4 | { 5 | [JsonProperty("subscriptionId")] 6 | public string SubscriptionId { get; set; } 7 | } -------------------------------------------------------------------------------- /server/dotnet/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotNetEnv; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace sample 7 | { 8 | public class Program 9 | { 10 | private const string StripeWebrootKey = "STATIC_DIR"; 11 | 12 | private const string DefaultStaticDir = "../../client"; 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 | // Load the .env file if one exists. 22 | DotNetEnv.Env.Load(); 23 | return Host.CreateDefaultBuilder(args) 24 | .ConfigureWebHostDefaults(webBuilder => 25 | { 26 | var webRoot = Environment.GetEnvironmentVariable(StripeWebrootKey); 27 | 28 | // If user forgets to set webroot, or improperly set - default to the current sample client folder. 29 | webRoot = webRoot ??= DefaultStaticDir; 30 | webBuilder.UseStartup(); 31 | 32 | // Setting web root here since client folder is reused across different server samples. 33 | webBuilder.UseWebRoot(webRoot); 34 | }); 35 | } 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /server/dotnet/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:42424", 7 | "sslPort": 4242 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "sample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:4242;http://localhost:42424", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/dotnet/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | ## Requirements 4 | - [.NET Core()]https://dotnet.microsoft.com/download) 5 | 6 | ## How to run 7 | 8 | 1. Run the application 9 | ``` 10 | dotnet run 11 | ``` 12 | 13 | 2. Go to `https://localhost:4242` or `http://localhost:42424` in your browser to see the demo 14 | -------------------------------------------------------------------------------- /server/dotnet/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Newtonsoft.Json.Serialization; 8 | 9 | namespace sample 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.Configure(stripeOptions => 23 | { 24 | stripeOptions.StripePublishableKey = Environment.GetEnvironmentVariable("STRIPE_PUBLISHABLE_KEY"); 25 | stripeOptions.StripeSecretKey = Environment.GetEnvironmentVariable("STRIPE_SECRET_KEY"); 26 | stripeOptions.StripeWebhookSecret = Environment.GetEnvironmentVariable("STRIPE_WEBHOOK_SECRET"); 27 | stripeOptions.SubscriptionPriceId = Environment.GetEnvironmentVariable("SUBSCRIPTION_PRICE_ID"); 28 | }); 29 | 30 | // Serialize JSON back in a way the sample JavaScript expects. 31 | services.AddControllersWithViews().AddNewtonsoftJson(options => 32 | { 33 | options.SerializerSettings.ContractResolver = new DefaultContractResolver 34 | { 35 | NamingStrategy = new SnakeCaseNamingStrategy() 36 | }; 37 | }); 38 | } 39 | 40 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 41 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 42 | { 43 | if (env.IsDevelopment()) 44 | { 45 | app.UseDeveloperExceptionPage(); 46 | } 47 | else 48 | { 49 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 50 | app.UseHsts(); 51 | } 52 | 53 | app.UseHttpsRedirection(); 54 | 55 | // Use this setting that index.html from web root is automatically shown. 56 | app.UseFileServer(); 57 | 58 | app.UseRouting(); 59 | 60 | app.UseAuthorization(); 61 | 62 | app.UseEndpoints(endpoints => 63 | { 64 | endpoints.MapControllerRoute( 65 | name: "default", 66 | pattern: "{controller=Home}/{action=Index}/{id?}"); 67 | }); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /server/dotnet/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/dotnet/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /server/dotnet/sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/go/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | An [Echo](https://echo.labstack.com/) implementation. 4 | 5 | ## Requirements 6 | 7 | - Go 8 | - [stripe-go](https://github.com/stripe/stripe-go) 9 | - [Echo](https://echo.labstack.com/guide/installation) 10 | - [godotenv](https://github.com/joho/godotenv) 11 | - [Configured .env file](../README.md) 12 | 13 | ## How to run 14 | 15 | 1. Install dependencies 16 | 17 | ``` 18 | go get -u github.com/labstack/echo/... 19 | go get github.com/joho/godotenv 20 | go get -u github.com/stripe/stripe-go 21 | go get github.com/foolin/goview 22 | ``` 23 | 24 | 2. Run the application 25 | 26 | ``` 27 | go run server.go 28 | ``` 29 | 30 | 3. Go to `localhost:4242` in your browser to see the demo 31 | -------------------------------------------------------------------------------- /server/go/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/foolin/goview/supports/echoview" 11 | "github.com/joho/godotenv" 12 | "github.com/labstack/echo" 13 | "github.com/labstack/echo/middleware" 14 | "github.com/stripe/stripe-go" 15 | "github.com/stripe/stripe-go/customer" 16 | "github.com/stripe/stripe-go/sub" 17 | "github.com/stripe/stripe-go/webhook" 18 | ) 19 | 20 | // CreateCustomerData represents data passed in request to create customer 21 | // object. 22 | type CreateCustomerData struct { 23 | PaymentMethodID string `json:"payment_method"` 24 | Email string `json:"email"` 25 | } 26 | 27 | // GetSubscriptionData represents data passed in request to retrieve 28 | // subscription. 29 | type GetSubscriptionData struct { 30 | SubscriptionID string `json:"subscriptionId"` 31 | } 32 | 33 | // PublicKey returned to client. 34 | type PublicKey struct { 35 | PublicKey string `json:"publicKey"` 36 | } 37 | 38 | func main() { 39 | err := godotenv.Load(".env") 40 | 41 | if err != nil { 42 | fmt.Println("Error loading .env file") 43 | } 44 | 45 | fmt.Println(os.Getenv("STRIPE_SECRET_KEY")) 46 | 47 | stripe.Key = os.Getenv("STRIPE_SECRET_KEY") 48 | e := echo.New() 49 | e.Use(middleware.Logger()) 50 | e.Use(middleware.Recover()) 51 | e.Renderer = echoview.Default() 52 | 53 | e.Static("/", os.Getenv("STATIC_DIR")) 54 | e.File("/", os.Getenv("STATIC_DIR") + "/index.html") 55 | 56 | e.GET("/public-key", func(c echo.Context) error { 57 | resp := &PublicKey{ 58 | PublicKey: os.Getenv("STRIPE_PUBLISHABLE_KEY"), 59 | } 60 | return c.JSON(http.StatusOK, resp) 61 | }) 62 | 63 | e.POST("/create-customer", func(c echo.Context) (err error) { 64 | request := new(CreateCustomerData) 65 | if err = c.Bind(request); err != nil { 66 | fmt.Println("Failed to parse data for create-customer") 67 | } 68 | 69 | // This creates a new Customer and attaches the PaymentMethod in one API 70 | // call. 71 | customerParams := &stripe.CustomerParams{ 72 | PaymentMethod: stripe.String(request.PaymentMethodID), 73 | Email: stripe.String(request.Email), 74 | InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{ 75 | DefaultPaymentMethod: stripe.String(request.PaymentMethodID), 76 | }, 77 | } 78 | customer, _ := customer.New(customerParams) 79 | 80 | items := []*stripe.SubscriptionItemsParams{ 81 | { 82 | Price: stripe.String(os.Getenv("SUBSCRIPTION_PRICE_ID")), 83 | }, 84 | } 85 | params := &stripe.SubscriptionParams{ 86 | Customer: stripe.String(customer.ID), 87 | Items: items, 88 | } 89 | params.AddExpand("latest_invoice.payment_intent") 90 | subscription, _ := sub.New(params) 91 | 92 | return c.JSON(http.StatusOK, subscription) 93 | }) 94 | 95 | e.POST("/subscription", func(c echo.Context) (err error) { 96 | request := new(GetSubscriptionData) 97 | if err = c.Bind(request); err != nil { 98 | fmt.Println("Failed to parse data for /subscription") 99 | } 100 | 101 | subscription, _ := sub.Get( 102 | request.SubscriptionID, 103 | nil, 104 | ) 105 | return c.JSON(http.StatusOK, subscription) 106 | }) 107 | 108 | 109 | e.POST("/webhook", func(c echo.Context) (err error) { 110 | request := c.Request() 111 | payload, err := ioutil.ReadAll(request.Body) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | var event stripe.Event 117 | 118 | webhookSecret := os.Getenv("STRIPE_WEBHOOK_SECRET") 119 | if webhookSecret != "" { 120 | event, err = webhook.ConstructEvent(payload, request.Header.Get("Stripe-Signature"), webhookSecret) 121 | if err != nil { 122 | return err 123 | } 124 | } else { 125 | err := json.Unmarshal(payload, &event) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | 131 | switch event.Type { 132 | case "customer.created": 133 | // Handle logic when a customer is created 134 | case "customer.updated": 135 | case "invoice.upcoming": 136 | case "invoice.created": 137 | case "invoice.finalized": 138 | case "invoice.payment_succeeded": 139 | case "invoice.payment_failed": 140 | case "customer.subscription.created": 141 | } 142 | 143 | if err != nil { 144 | return err 145 | } 146 | 147 | return c.JSON(http.StatusOK, event) 148 | }) 149 | 150 | e.Logger.Fatal(e.Start("localhost:4242")) 151 | } 152 | -------------------------------------------------------------------------------- /server/java/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | ## Requirements 4 | 5 | - Maven 6 | - Java 7 | - [Configured .env file](../README.md) 8 | 9 | 1. Build the package 10 | 11 | ``` 12 | mvn package 13 | ``` 14 | 15 | 2. Run the application 16 | 17 | ``` 18 | java -cp target/billing-subscription-quickstart-1.0.0-SNAPSHOT-jar-with-dependencies.jar com.stripe.sample.Server 19 | ``` 20 | 21 | 3. Go to `localhost:4242` in your browser to see the demo 22 | -------------------------------------------------------------------------------- /server/java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | com.stripe.sample 7 | billing-subscription-quickstart 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 12 | 13 | org.slf4j 14 | slf4j-simple 15 | 1.7.21 16 | 17 | 18 | com.sparkjava 19 | spark-core 20 | 2.8.0 21 | 22 | 23 | com.google.code.gson 24 | gson 25 | 2.3.1 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.8 31 | provided 32 | 33 | 34 | com.stripe 35 | stripe-java 36 | 19.8.0 37 | 38 | 39 | io.github.cdimascio 40 | java-dotenv 41 | 5.1.1 42 | 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-compiler-plugin 49 | 2.3.2 50 | 51 | 1.8 52 | 1.8 53 | 54 | 55 | 56 | maven-assembly-plugin 57 | 58 | 59 | package 60 | 61 | single 62 | 63 | 64 | 65 | 66 | 67 | 68 | jar-with-dependencies 69 | 70 | 71 | 72 | Server 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /server/java/src/main/java/com/stripe/sample/Server.java: -------------------------------------------------------------------------------- 1 | package com.stripe.sample; 2 | 3 | import static spark.Spark.get; 4 | import static spark.Spark.port; 5 | import static spark.Spark.post; 6 | import static spark.Spark.staticFiles; 7 | 8 | import java.nio.file.Paths; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import com.google.gson.Gson; 14 | import com.google.gson.JsonObject; 15 | import com.google.gson.annotations.SerializedName; 16 | import com.stripe.Stripe; 17 | import com.stripe.model.Customer; 18 | import com.stripe.model.Event; 19 | import com.stripe.model.EventDataObjectDeserializer; 20 | import com.stripe.model.Invoice; 21 | import com.stripe.model.StripeObject; 22 | import com.stripe.model.Subscription; 23 | import com.stripe.param.CustomerCreateParams; 24 | import com.stripe.param.SubscriptionCreateParams; 25 | import com.stripe.exception.SignatureVerificationException; 26 | import com.stripe.net.Webhook; 27 | 28 | import io.github.cdimascio.dotenv.Dotenv; 29 | 30 | public class Server { 31 | private static Gson gson = new Gson(); 32 | 33 | static class CreatePaymentBody { 34 | @SerializedName("payment_method") 35 | String paymentMethod; 36 | @SerializedName("email") 37 | String email; 38 | 39 | public String getPaymentMethod() { 40 | return paymentMethod; 41 | } 42 | 43 | public String getEmail() { 44 | return email; 45 | } 46 | } 47 | 48 | static class CreateSubscriptionBody { 49 | @SerializedName("subscriptionId") 50 | String subscriptionId; 51 | 52 | public String getSubscriptionId() { 53 | return subscriptionId; 54 | } 55 | } 56 | 57 | public static void main(String[] args) { 58 | port(4242); 59 | Dotenv dotenv = Dotenv.load(); 60 | Stripe.apiKey = dotenv.get("STRIPE_SECRET_KEY"); 61 | 62 | staticFiles.externalLocation( 63 | Paths.get(Paths.get("").toAbsolutePath().toString(), dotenv.get("STATIC_DIR")).normalize().toString()); 64 | 65 | get("/public-key", (request, response) -> { 66 | response.type("application/json"); 67 | JsonObject publicKey = new JsonObject(); 68 | publicKey.addProperty("publicKey", dotenv.get("STRIPE_PUBLISHABLE_KEY")); 69 | return publicKey.toString(); 70 | }); 71 | 72 | post("/create-customer", (request, response) -> { 73 | response.type("application/json"); 74 | 75 | CreatePaymentBody postBody = gson.fromJson(request.body(), CreatePaymentBody.class); 76 | // This creates a new Customer and attaches the PaymentMethod in one API call. 77 | CustomerCreateParams customerParams = 78 | CustomerCreateParams.builder() 79 | .setPaymentMethod(postBody.getPaymentMethod()) 80 | .setEmail(postBody.getEmail()) 81 | .setInvoiceSettings( 82 | CustomerCreateParams.InvoiceSettings.builder() 83 | .setDefaultPaymentMethod(postBody.getPaymentMethod()) 84 | .build()) 85 | .build(); 86 | 87 | Customer customer = Customer.create(customerParams); 88 | 89 | //Subscribe the customer to a price 90 | SubscriptionCreateParams subscriptionParams = 91 | SubscriptionCreateParams.builder() 92 | .addItem( 93 | SubscriptionCreateParams.Item.builder() 94 | .setPrice(dotenv.get("SUBSCRIPTION_PRICE_ID")) 95 | .build()) 96 | .setCustomer(customer.getId()) 97 | .addAllExpand(Arrays.asList("latest_invoice.payment_intent")) 98 | .build(); 99 | 100 | Subscription subscription = Subscription.create(subscriptionParams); 101 | return subscription.toJson(); 102 | }); 103 | 104 | post("/subscription", (request, response) -> { 105 | response.type("application/json"); 106 | 107 | CreateSubscriptionBody postBody = gson.fromJson(request.body(), CreateSubscriptionBody.class); 108 | return Subscription.retrieve(postBody.getSubscriptionId()).toJson(); 109 | }); 110 | 111 | post("/webhook", (request, response) -> { 112 | String payload = request.body(); 113 | String sigHeader = request.headers("Stripe-Signature"); 114 | String endpointSecret = dotenv.get("STRIPE_WEBHOOK_SECRET"); 115 | Event event = null; 116 | 117 | try { 118 | event = Webhook.constructEvent(payload, sigHeader, endpointSecret); 119 | } catch (SignatureVerificationException e) { 120 | // Invalid signature 121 | response.status(400); 122 | return ""; 123 | } 124 | 125 | // Deserialize the nested object inside the event 126 | EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); 127 | StripeObject stripeObject = null; 128 | if (dataObjectDeserializer.getObject().isPresent()) { 129 | stripeObject = dataObjectDeserializer.getObject().get(); 130 | } else { 131 | // Deserialization failed, probably due to an API version mismatch. 132 | // Refer to the Javadoc documentation on `EventDataObjectDeserializer` for 133 | // instructions on how to handle this case, or return an error here. 134 | } 135 | 136 | switch (event.getType()) { 137 | case "customer.created": 138 | // Customer customer = (Customer) stripeObject; 139 | // System.out.println(customer.toJson()); 140 | break; 141 | case "customer.updated": 142 | // Customer customer = (Customer) stripeObject; 143 | // System.out.println(customer.toJson()); 144 | break; 145 | case "invoice.upcoming": 146 | // Invoice invoice = (Invoice) stripeObject; 147 | // System.out.println(invoice.toJson()); 148 | break; 149 | case "invoice.created": 150 | // Invoice invoice = (Invoice) stripeObject; 151 | // System.out.println(invoice.toJson()); 152 | break; 153 | case "invoice.finalized": 154 | // Invoice invoice = (Invoice) stripeObject; 155 | // System.out.println(invoice.toJson()); 156 | break; 157 | case "invoice.payment_succeeded": 158 | // Invoice invoice = (Invoice) stripeObject; 159 | // System.out.println(invoice.toJson()); 160 | break; 161 | case "invoice.payment_failed": 162 | // Invoice invoice = (Invoice) stripeObject; 163 | // System.out.println(invoice.toJson()); 164 | break; 165 | case "customer.subscription.created": 166 | Subscription subscription = (Subscription) stripeObject; 167 | System.out.println(subscription.toJson()); 168 | break; 169 | default: 170 | // Unexpected event type 171 | response.status(400); 172 | return ""; 173 | } 174 | 175 | response.status(200); 176 | return ""; 177 | }); 178 | } 179 | } -------------------------------------------------------------------------------- /server/node/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | An [Express server](http://expressjs.com) implementation. 4 | 5 | ## Requirements 6 | 7 | - Node v10+ 8 | - [Configured .env file](../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Install dependencies 13 | 14 | ``` 15 | npm install 16 | ``` 17 | 18 | 2. Run the application 19 | 20 | ``` 21 | npm start 22 | ``` 23 | 24 | 3. Go to `localhost:4242` to see the demo 25 | -------------------------------------------------------------------------------- /server/node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-billing-quickstart-demo", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "array-flatten": { 17 | "version": "1.1.1", 18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 20 | }, 21 | "body-parser": { 22 | "version": "1.19.0", 23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 24 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 25 | "requires": { 26 | "bytes": "3.1.0", 27 | "content-type": "~1.0.4", 28 | "debug": "2.6.9", 29 | "depd": "~1.1.2", 30 | "http-errors": "1.7.2", 31 | "iconv-lite": "0.4.24", 32 | "on-finished": "~2.3.0", 33 | "qs": "6.7.0", 34 | "raw-body": "2.4.0", 35 | "type-is": "~1.6.17" 36 | } 37 | }, 38 | "bytes": { 39 | "version": "3.1.0", 40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 41 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 42 | }, 43 | "content-disposition": { 44 | "version": "0.5.3", 45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 46 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 47 | "requires": { 48 | "safe-buffer": "5.1.2" 49 | } 50 | }, 51 | "content-type": { 52 | "version": "1.0.4", 53 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 54 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 55 | }, 56 | "cookie": { 57 | "version": "0.4.0", 58 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 59 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 60 | }, 61 | "cookie-signature": { 62 | "version": "1.0.6", 63 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 64 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 65 | }, 66 | "debug": { 67 | "version": "2.6.9", 68 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 69 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 70 | "requires": { 71 | "ms": "2.0.0" 72 | } 73 | }, 74 | "depd": { 75 | "version": "1.1.2", 76 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 77 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 78 | }, 79 | "destroy": { 80 | "version": "1.0.4", 81 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 82 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 83 | }, 84 | "dotenv": { 85 | "version": "8.0.0", 86 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", 87 | "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" 88 | }, 89 | "ee-first": { 90 | "version": "1.1.1", 91 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 92 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 93 | }, 94 | "encodeurl": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 97 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 98 | }, 99 | "escape-html": { 100 | "version": "1.0.3", 101 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 102 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 103 | }, 104 | "etag": { 105 | "version": "1.8.1", 106 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 107 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 108 | }, 109 | "express": { 110 | "version": "4.17.1", 111 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 112 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 113 | "requires": { 114 | "accepts": "~1.3.7", 115 | "array-flatten": "1.1.1", 116 | "body-parser": "1.19.0", 117 | "content-disposition": "0.5.3", 118 | "content-type": "~1.0.4", 119 | "cookie": "0.4.0", 120 | "cookie-signature": "1.0.6", 121 | "debug": "2.6.9", 122 | "depd": "~1.1.2", 123 | "encodeurl": "~1.0.2", 124 | "escape-html": "~1.0.3", 125 | "etag": "~1.8.1", 126 | "finalhandler": "~1.1.2", 127 | "fresh": "0.5.2", 128 | "merge-descriptors": "1.0.1", 129 | "methods": "~1.1.2", 130 | "on-finished": "~2.3.0", 131 | "parseurl": "~1.3.3", 132 | "path-to-regexp": "0.1.7", 133 | "proxy-addr": "~2.0.5", 134 | "qs": "6.7.0", 135 | "range-parser": "~1.2.1", 136 | "safe-buffer": "5.1.2", 137 | "send": "0.17.1", 138 | "serve-static": "1.14.1", 139 | "setprototypeof": "1.1.1", 140 | "statuses": "~1.5.0", 141 | "type-is": "~1.6.18", 142 | "utils-merge": "1.0.1", 143 | "vary": "~1.1.2" 144 | } 145 | }, 146 | "finalhandler": { 147 | "version": "1.1.2", 148 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 149 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 150 | "requires": { 151 | "debug": "2.6.9", 152 | "encodeurl": "~1.0.2", 153 | "escape-html": "~1.0.3", 154 | "on-finished": "~2.3.0", 155 | "parseurl": "~1.3.3", 156 | "statuses": "~1.5.0", 157 | "unpipe": "~1.0.0" 158 | } 159 | }, 160 | "forwarded": { 161 | "version": "0.1.2", 162 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 163 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 164 | }, 165 | "fresh": { 166 | "version": "0.5.2", 167 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 168 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 169 | }, 170 | "http-errors": { 171 | "version": "1.7.2", 172 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 173 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 174 | "requires": { 175 | "depd": "~1.1.2", 176 | "inherits": "2.0.3", 177 | "setprototypeof": "1.1.1", 178 | "statuses": ">= 1.5.0 < 2", 179 | "toidentifier": "1.0.0" 180 | } 181 | }, 182 | "iconv-lite": { 183 | "version": "0.4.24", 184 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 185 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 186 | "requires": { 187 | "safer-buffer": ">= 2.1.2 < 3" 188 | } 189 | }, 190 | "inherits": { 191 | "version": "2.0.3", 192 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 193 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 194 | }, 195 | "ipaddr.js": { 196 | "version": "1.9.0", 197 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 198 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 199 | }, 200 | "lodash.isplainobject": { 201 | "version": "4.0.6", 202 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 203 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 204 | }, 205 | "media-typer": { 206 | "version": "0.3.0", 207 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 208 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 209 | }, 210 | "merge-descriptors": { 211 | "version": "1.0.1", 212 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 213 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 214 | }, 215 | "methods": { 216 | "version": "1.1.2", 217 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 218 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 219 | }, 220 | "mime": { 221 | "version": "1.6.0", 222 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 223 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 224 | }, 225 | "mime-db": { 226 | "version": "1.40.0", 227 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 228 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 229 | }, 230 | "mime-types": { 231 | "version": "2.1.24", 232 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 233 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 234 | "requires": { 235 | "mime-db": "1.40.0" 236 | } 237 | }, 238 | "ms": { 239 | "version": "2.0.0", 240 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 241 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 242 | }, 243 | "negotiator": { 244 | "version": "0.6.2", 245 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 246 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 247 | }, 248 | "on-finished": { 249 | "version": "2.3.0", 250 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 251 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 252 | "requires": { 253 | "ee-first": "1.1.1" 254 | } 255 | }, 256 | "parseurl": { 257 | "version": "1.3.3", 258 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 259 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 260 | }, 261 | "path-to-regexp": { 262 | "version": "0.1.7", 263 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 264 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 265 | }, 266 | "proxy-addr": { 267 | "version": "2.0.5", 268 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 269 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 270 | "requires": { 271 | "forwarded": "~0.1.2", 272 | "ipaddr.js": "1.9.0" 273 | } 274 | }, 275 | "qs": { 276 | "version": "6.7.0", 277 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 278 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 279 | }, 280 | "range-parser": { 281 | "version": "1.2.1", 282 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 283 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 284 | }, 285 | "raw-body": { 286 | "version": "2.4.0", 287 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 288 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 289 | "requires": { 290 | "bytes": "3.1.0", 291 | "http-errors": "1.7.2", 292 | "iconv-lite": "0.4.24", 293 | "unpipe": "1.0.0" 294 | } 295 | }, 296 | "safe-buffer": { 297 | "version": "5.1.2", 298 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 299 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 300 | }, 301 | "safer-buffer": { 302 | "version": "2.1.2", 303 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 304 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 305 | }, 306 | "send": { 307 | "version": "0.17.1", 308 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 309 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 310 | "requires": { 311 | "debug": "2.6.9", 312 | "depd": "~1.1.2", 313 | "destroy": "~1.0.4", 314 | "encodeurl": "~1.0.2", 315 | "escape-html": "~1.0.3", 316 | "etag": "~1.8.1", 317 | "fresh": "0.5.2", 318 | "http-errors": "~1.7.2", 319 | "mime": "1.6.0", 320 | "ms": "2.1.1", 321 | "on-finished": "~2.3.0", 322 | "range-parser": "~1.2.1", 323 | "statuses": "~1.5.0" 324 | }, 325 | "dependencies": { 326 | "ms": { 327 | "version": "2.1.1", 328 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 329 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 330 | } 331 | } 332 | }, 333 | "serve-static": { 334 | "version": "1.14.1", 335 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 336 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 337 | "requires": { 338 | "encodeurl": "~1.0.2", 339 | "escape-html": "~1.0.3", 340 | "parseurl": "~1.3.3", 341 | "send": "0.17.1" 342 | } 343 | }, 344 | "setprototypeof": { 345 | "version": "1.1.1", 346 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 347 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 348 | }, 349 | "statuses": { 350 | "version": "1.5.0", 351 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 352 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 353 | }, 354 | "stripe": { 355 | "version": "7.4.0", 356 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-7.4.0.tgz", 357 | "integrity": "sha512-eurSZJw45MvnV7PjmFHMgJMkCihHgqGHr11OHpFdMh+5CCyYvbVlA5uP5VoVQakhYjSLCObs0dbXtGYhIAMKvw==", 358 | "requires": { 359 | "lodash.isplainobject": "^4.0.6", 360 | "qs": "^6.6.0", 361 | "safe-buffer": "^5.1.1", 362 | "uuid": "^3.3.2" 363 | } 364 | }, 365 | "toidentifier": { 366 | "version": "1.0.0", 367 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 368 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 369 | }, 370 | "type-is": { 371 | "version": "1.6.18", 372 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 373 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 374 | "requires": { 375 | "media-typer": "0.3.0", 376 | "mime-types": "~2.1.24" 377 | } 378 | }, 379 | "unpipe": { 380 | "version": "1.0.0", 381 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 382 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 383 | }, 384 | "utils-merge": { 385 | "version": "1.0.1", 386 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 387 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 388 | }, 389 | "uuid": { 390 | "version": "3.3.2", 391 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 392 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 393 | }, 394 | "vary": { 395 | "version": "1.1.2", 396 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 397 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 398 | } 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /server/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-billing-quickstart-demo", 3 | "version": "1.0.0", 4 | "description": "A Stripe Stripe Billing charging for subscriptions demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "stripe-demos", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "dotenv": "^8.0.0", 15 | "express": "^4.17.1", 16 | "stripe": "^7.4.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/node/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const { resolve } = require('path'); 4 | // Replace if using a different env file or config 5 | const env = require('dotenv').config({ path: './.env' }); 6 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 7 | 8 | app.use(express.static(process.env.STATIC_DIR)); 9 | 10 | app.use( 11 | express.json({ 12 | // We need the raw body to verify webhook signatures. 13 | // Let's compute it only when hitting the Stripe webhook endpoint. 14 | verify: function(req, res, buf) { 15 | if (req.originalUrl.startsWith('/webhook')) { 16 | req.rawBody = buf.toString(); 17 | } 18 | } 19 | }) 20 | ); 21 | 22 | app.get('/', (req, res) => { 23 | const path = resolve(process.env.STATIC_DIR + '/index.html'); 24 | res.sendFile(path); 25 | }); 26 | 27 | app.get('/public-key', (req, res) => { 28 | res.send({ publicKey: process.env.STRIPE_PUBLISHABLE_KEY }); 29 | }); 30 | 31 | app.post('/create-customer', async (req, res) => { 32 | // This creates a new Customer and attaches 33 | // the PaymentMethod to be default for invoice in one API call. 34 | const customer = await stripe.customers.create({ 35 | payment_method: req.body.payment_method, 36 | email: req.body.email, 37 | invoice_settings: { 38 | default_payment_method: req.body.payment_method 39 | } 40 | }); 41 | // At this point, associate the ID of the Customer object with your 42 | // own internal representation of a customer, if you have one. 43 | const subscription = await stripe.subscriptions.create({ 44 | customer: customer.id, 45 | items: [{ price: process.env.SUBSCRIPTION_PRICE_ID }], 46 | expand: ['latest_invoice.payment_intent'] 47 | }); 48 | res.send(subscription); 49 | }); 50 | 51 | app.post('/subscription', async (req, res) => { 52 | let subscription = await stripe.subscriptions.retrieve( 53 | req.body.subscriptionId 54 | ); 55 | res.send(subscription); 56 | }); 57 | 58 | // Webhook handler for asynchronous events. 59 | app.post('/webhook', async (req, res) => { 60 | let data; 61 | let eventType; 62 | // Check if webhook signing is configured. 63 | if (process.env.STRIPE_WEBHOOK_SECRET) { 64 | // Retrieve the event by verifying the signature using the raw body and secret. 65 | let event; 66 | let signature = req.headers['stripe-signature']; 67 | 68 | try { 69 | event = stripe.webhooks.constructEvent( 70 | req.rawBody, 71 | signature, 72 | process.env.STRIPE_WEBHOOK_SECRET 73 | ); 74 | } catch (err) { 75 | console.log(`⚠️ Webhook signature verification failed.`); 76 | return res.sendStatus(400); 77 | } 78 | // Extract the object from the event. 79 | dataObject = event.data.object; 80 | eventType = event.type; 81 | 82 | // Handle the event 83 | // Review important events for Billing webhooks 84 | // https://stripe.com/docs/billing/webhooks 85 | // Remove comment to see the various objects sent for this sample 86 | switch (event.type) { 87 | case 'customer.created': 88 | // console.log(dataObject); 89 | break; 90 | case 'customer.updated': 91 | // console.log(dataObject); 92 | break; 93 | case 'invoice.upcoming': 94 | // console.log(dataObject); 95 | break; 96 | case 'invoice.created': 97 | // console.log(dataObject); 98 | break; 99 | case 'invoice.finalized': 100 | // console.log(dataObject); 101 | break; 102 | case 'invoice.payment_succeeded': 103 | // console.log(dataObject); 104 | break; 105 | case 'invoice.payment_failed': 106 | // console.log(dataObject); 107 | break; 108 | case 'customer.subscription.created': 109 | // console.log(dataObject); 110 | break; 111 | // ... handle other event types 112 | default: 113 | // Unexpected event type 114 | return res.status(400).end(); 115 | } 116 | } else { 117 | // Webhook signing is recommended, but if the secret is not configured in `config.js`, 118 | // retrieve the event data directly from the request body. 119 | data = req.body.data; 120 | eventType = req.body.type; 121 | } 122 | 123 | res.sendStatus(200); 124 | }); 125 | 126 | app.listen(4242, () => console.log(`Node server listening on port ${4242}!`)); 127 | -------------------------------------------------------------------------------- /server/php/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ index.php [QSA,L] -------------------------------------------------------------------------------- /server/php/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | ## Requirements 4 | 5 | - PHP >= 7.1.3 6 | - Composer 7 | - [Slim](http://www.slimframework.com/) 8 | 9 | ## How to run 10 | 11 | 1. Install dependencies 12 | 13 | ``` 14 | composer install 15 | ``` 16 | 17 | 2. Run the application 18 | 19 | ``` 20 | composer start 21 | ``` 22 | 23 | 3. Go to `localhost:4242` in your browser to see the demo 24 | -------------------------------------------------------------------------------- /server/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "slim/slim": "^3.12", 4 | "vlucas/phpdotenv": "^3.4", 5 | "stripe/stripe-php": "^6.40", 6 | "monolog/monolog": "^1.17" 7 | }, 8 | "scripts": { 9 | "start": "php -S localhost:4242 index.php" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/php/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "4e2fe5a120150c19ca2bf91e92035d7d", 8 | "packages": [ 9 | { 10 | "name": "container-interop/container-interop", 11 | "version": "1.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/container-interop/container-interop.git", 15 | "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", 20 | "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "psr/container": "^1.0" 25 | }, 26 | "type": "library", 27 | "autoload": { 28 | "psr-4": { 29 | "Interop\\Container\\": "src/Interop/Container/" 30 | } 31 | }, 32 | "notification-url": "https://packagist.org/downloads/", 33 | "license": [ 34 | "MIT" 35 | ], 36 | "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", 37 | "homepage": "https://github.com/container-interop/container-interop", 38 | "time": "2017-02-14T19:40:03+00:00" 39 | }, 40 | { 41 | "name": "monolog/monolog", 42 | "version": "1.24.0", 43 | "source": { 44 | "type": "git", 45 | "url": "https://github.com/Seldaek/monolog.git", 46 | "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" 47 | }, 48 | "dist": { 49 | "type": "zip", 50 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", 51 | "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", 52 | "shasum": "" 53 | }, 54 | "require": { 55 | "php": ">=5.3.0", 56 | "psr/log": "~1.0" 57 | }, 58 | "provide": { 59 | "psr/log-implementation": "1.0.0" 60 | }, 61 | "require-dev": { 62 | "aws/aws-sdk-php": "^2.4.9 || ^3.0", 63 | "doctrine/couchdb": "~1.0@dev", 64 | "graylog2/gelf-php": "~1.0", 65 | "jakub-onderka/php-parallel-lint": "0.9", 66 | "php-amqplib/php-amqplib": "~2.4", 67 | "php-console/php-console": "^3.1.3", 68 | "phpunit/phpunit": "~4.5", 69 | "phpunit/phpunit-mock-objects": "2.3.0", 70 | "ruflin/elastica": ">=0.90 <3.0", 71 | "sentry/sentry": "^0.13", 72 | "swiftmailer/swiftmailer": "^5.3|^6.0" 73 | }, 74 | "suggest": { 75 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", 76 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server", 77 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", 78 | "ext-mongo": "Allow sending log messages to a MongoDB server", 79 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", 80 | "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", 81 | "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", 82 | "php-console/php-console": "Allow sending log messages to Google Chrome", 83 | "rollbar/rollbar": "Allow sending log messages to Rollbar", 84 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server", 85 | "sentry/sentry": "Allow sending log messages to a Sentry server" 86 | }, 87 | "type": "library", 88 | "extra": { 89 | "branch-alias": { 90 | "dev-master": "2.0.x-dev" 91 | } 92 | }, 93 | "autoload": { 94 | "psr-4": { 95 | "Monolog\\": "src/Monolog" 96 | } 97 | }, 98 | "notification-url": "https://packagist.org/downloads/", 99 | "license": [ 100 | "MIT" 101 | ], 102 | "authors": [ 103 | { 104 | "name": "Jordi Boggiano", 105 | "email": "j.boggiano@seld.be", 106 | "homepage": "http://seld.be" 107 | } 108 | ], 109 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services", 110 | "homepage": "http://github.com/Seldaek/monolog", 111 | "keywords": [ 112 | "log", 113 | "logging", 114 | "psr-3" 115 | ], 116 | "time": "2018-11-05T09:00:11+00:00" 117 | }, 118 | { 119 | "name": "nikic/fast-route", 120 | "version": "v1.3.0", 121 | "source": { 122 | "type": "git", 123 | "url": "https://github.com/nikic/FastRoute.git", 124 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812" 125 | }, 126 | "dist": { 127 | "type": "zip", 128 | "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", 129 | "reference": "181d480e08d9476e61381e04a71b34dc0432e812", 130 | "shasum": "" 131 | }, 132 | "require": { 133 | "php": ">=5.4.0" 134 | }, 135 | "require-dev": { 136 | "phpunit/phpunit": "^4.8.35|~5.7" 137 | }, 138 | "type": "library", 139 | "autoload": { 140 | "psr-4": { 141 | "FastRoute\\": "src/" 142 | }, 143 | "files": [ 144 | "src/functions.php" 145 | ] 146 | }, 147 | "notification-url": "https://packagist.org/downloads/", 148 | "license": [ 149 | "BSD-3-Clause" 150 | ], 151 | "authors": [ 152 | { 153 | "name": "Nikita Popov", 154 | "email": "nikic@php.net" 155 | } 156 | ], 157 | "description": "Fast request router for PHP", 158 | "keywords": [ 159 | "router", 160 | "routing" 161 | ], 162 | "time": "2018-02-13T20:26:39+00:00" 163 | }, 164 | { 165 | "name": "phpoption/phpoption", 166 | "version": "1.5.0", 167 | "source": { 168 | "type": "git", 169 | "url": "https://github.com/schmittjoh/php-option.git", 170 | "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" 171 | }, 172 | "dist": { 173 | "type": "zip", 174 | "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", 175 | "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", 176 | "shasum": "" 177 | }, 178 | "require": { 179 | "php": ">=5.3.0" 180 | }, 181 | "require-dev": { 182 | "phpunit/phpunit": "4.7.*" 183 | }, 184 | "type": "library", 185 | "extra": { 186 | "branch-alias": { 187 | "dev-master": "1.3-dev" 188 | } 189 | }, 190 | "autoload": { 191 | "psr-0": { 192 | "PhpOption\\": "src/" 193 | } 194 | }, 195 | "notification-url": "https://packagist.org/downloads/", 196 | "license": [ 197 | "Apache2" 198 | ], 199 | "authors": [ 200 | { 201 | "name": "Johannes M. Schmitt", 202 | "email": "schmittjoh@gmail.com" 203 | } 204 | ], 205 | "description": "Option Type for PHP", 206 | "keywords": [ 207 | "language", 208 | "option", 209 | "php", 210 | "type" 211 | ], 212 | "time": "2015-07-25T16:39:46+00:00" 213 | }, 214 | { 215 | "name": "pimple/pimple", 216 | "version": "v3.2.3", 217 | "source": { 218 | "type": "git", 219 | "url": "https://github.com/silexphp/Pimple.git", 220 | "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" 221 | }, 222 | "dist": { 223 | "type": "zip", 224 | "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", 225 | "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", 226 | "shasum": "" 227 | }, 228 | "require": { 229 | "php": ">=5.3.0", 230 | "psr/container": "^1.0" 231 | }, 232 | "require-dev": { 233 | "symfony/phpunit-bridge": "^3.2" 234 | }, 235 | "type": "library", 236 | "extra": { 237 | "branch-alias": { 238 | "dev-master": "3.2.x-dev" 239 | } 240 | }, 241 | "autoload": { 242 | "psr-0": { 243 | "Pimple": "src/" 244 | } 245 | }, 246 | "notification-url": "https://packagist.org/downloads/", 247 | "license": [ 248 | "MIT" 249 | ], 250 | "authors": [ 251 | { 252 | "name": "Fabien Potencier", 253 | "email": "fabien@symfony.com" 254 | } 255 | ], 256 | "description": "Pimple, a simple Dependency Injection Container", 257 | "homepage": "http://pimple.sensiolabs.org", 258 | "keywords": [ 259 | "container", 260 | "dependency injection" 261 | ], 262 | "time": "2018-01-21T07:42:36+00:00" 263 | }, 264 | { 265 | "name": "psr/container", 266 | "version": "1.0.0", 267 | "source": { 268 | "type": "git", 269 | "url": "https://github.com/php-fig/container.git", 270 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 271 | }, 272 | "dist": { 273 | "type": "zip", 274 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 275 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 276 | "shasum": "" 277 | }, 278 | "require": { 279 | "php": ">=5.3.0" 280 | }, 281 | "type": "library", 282 | "extra": { 283 | "branch-alias": { 284 | "dev-master": "1.0.x-dev" 285 | } 286 | }, 287 | "autoload": { 288 | "psr-4": { 289 | "Psr\\Container\\": "src/" 290 | } 291 | }, 292 | "notification-url": "https://packagist.org/downloads/", 293 | "license": [ 294 | "MIT" 295 | ], 296 | "authors": [ 297 | { 298 | "name": "PHP-FIG", 299 | "homepage": "http://www.php-fig.org/" 300 | } 301 | ], 302 | "description": "Common Container Interface (PHP FIG PSR-11)", 303 | "homepage": "https://github.com/php-fig/container", 304 | "keywords": [ 305 | "PSR-11", 306 | "container", 307 | "container-interface", 308 | "container-interop", 309 | "psr" 310 | ], 311 | "time": "2017-02-14T16:28:37+00:00" 312 | }, 313 | { 314 | "name": "psr/http-message", 315 | "version": "1.0.1", 316 | "source": { 317 | "type": "git", 318 | "url": "https://github.com/php-fig/http-message.git", 319 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 320 | }, 321 | "dist": { 322 | "type": "zip", 323 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 324 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 325 | "shasum": "" 326 | }, 327 | "require": { 328 | "php": ">=5.3.0" 329 | }, 330 | "type": "library", 331 | "extra": { 332 | "branch-alias": { 333 | "dev-master": "1.0.x-dev" 334 | } 335 | }, 336 | "autoload": { 337 | "psr-4": { 338 | "Psr\\Http\\Message\\": "src/" 339 | } 340 | }, 341 | "notification-url": "https://packagist.org/downloads/", 342 | "license": [ 343 | "MIT" 344 | ], 345 | "authors": [ 346 | { 347 | "name": "PHP-FIG", 348 | "homepage": "http://www.php-fig.org/" 349 | } 350 | ], 351 | "description": "Common interface for HTTP messages", 352 | "homepage": "https://github.com/php-fig/http-message", 353 | "keywords": [ 354 | "http", 355 | "http-message", 356 | "psr", 357 | "psr-7", 358 | "request", 359 | "response" 360 | ], 361 | "time": "2016-08-06T14:39:51+00:00" 362 | }, 363 | { 364 | "name": "psr/log", 365 | "version": "1.1.0", 366 | "source": { 367 | "type": "git", 368 | "url": "https://github.com/php-fig/log.git", 369 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" 370 | }, 371 | "dist": { 372 | "type": "zip", 373 | "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 374 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 375 | "shasum": "" 376 | }, 377 | "require": { 378 | "php": ">=5.3.0" 379 | }, 380 | "type": "library", 381 | "extra": { 382 | "branch-alias": { 383 | "dev-master": "1.0.x-dev" 384 | } 385 | }, 386 | "autoload": { 387 | "psr-4": { 388 | "Psr\\Log\\": "Psr/Log/" 389 | } 390 | }, 391 | "notification-url": "https://packagist.org/downloads/", 392 | "license": [ 393 | "MIT" 394 | ], 395 | "authors": [ 396 | { 397 | "name": "PHP-FIG", 398 | "homepage": "http://www.php-fig.org/" 399 | } 400 | ], 401 | "description": "Common interface for logging libraries", 402 | "homepage": "https://github.com/php-fig/log", 403 | "keywords": [ 404 | "log", 405 | "psr", 406 | "psr-3" 407 | ], 408 | "time": "2018-11-20T15:27:04+00:00" 409 | }, 410 | { 411 | "name": "slim/slim", 412 | "version": "3.12.1", 413 | "source": { 414 | "type": "git", 415 | "url": "https://github.com/slimphp/Slim.git", 416 | "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b" 417 | }, 418 | "dist": { 419 | "type": "zip", 420 | "url": "https://api.github.com/repos/slimphp/Slim/zipball/eaee12ef8d0750db62b8c548016d82fb33addb6b", 421 | "reference": "eaee12ef8d0750db62b8c548016d82fb33addb6b", 422 | "shasum": "" 423 | }, 424 | "require": { 425 | "container-interop/container-interop": "^1.2", 426 | "nikic/fast-route": "^1.0", 427 | "php": ">=5.5.0", 428 | "pimple/pimple": "^3.0", 429 | "psr/container": "^1.0", 430 | "psr/http-message": "^1.0" 431 | }, 432 | "provide": { 433 | "psr/http-message-implementation": "1.0" 434 | }, 435 | "require-dev": { 436 | "phpunit/phpunit": "^4.0", 437 | "squizlabs/php_codesniffer": "^2.5" 438 | }, 439 | "type": "library", 440 | "autoload": { 441 | "psr-4": { 442 | "Slim\\": "Slim" 443 | } 444 | }, 445 | "notification-url": "https://packagist.org/downloads/", 446 | "license": [ 447 | "MIT" 448 | ], 449 | "authors": [ 450 | { 451 | "name": "Rob Allen", 452 | "email": "rob@akrabat.com", 453 | "homepage": "http://akrabat.com" 454 | }, 455 | { 456 | "name": "Josh Lockhart", 457 | "email": "hello@joshlockhart.com", 458 | "homepage": "https://joshlockhart.com" 459 | }, 460 | { 461 | "name": "Gabriel Manricks", 462 | "email": "gmanricks@me.com", 463 | "homepage": "http://gabrielmanricks.com" 464 | }, 465 | { 466 | "name": "Andrew Smith", 467 | "email": "a.smith@silentworks.co.uk", 468 | "homepage": "http://silentworks.co.uk" 469 | } 470 | ], 471 | "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", 472 | "homepage": "https://slimframework.com", 473 | "keywords": [ 474 | "api", 475 | "framework", 476 | "micro", 477 | "router" 478 | ], 479 | "time": "2019-04-16T16:47:29+00:00" 480 | }, 481 | { 482 | "name": "stripe/stripe-php", 483 | "version": "v6.40.0", 484 | "source": { 485 | "type": "git", 486 | "url": "https://github.com/stripe/stripe-php.git", 487 | "reference": "9c22ffab790ef4dae0f371929de50e8b53c9ec8d" 488 | }, 489 | "dist": { 490 | "type": "zip", 491 | "url": "https://api.github.com/repos/stripe/stripe-php/zipball/9c22ffab790ef4dae0f371929de50e8b53c9ec8d", 492 | "reference": "9c22ffab790ef4dae0f371929de50e8b53c9ec8d", 493 | "shasum": "" 494 | }, 495 | "require": { 496 | "ext-curl": "*", 497 | "ext-json": "*", 498 | "ext-mbstring": "*", 499 | "php": ">=5.4.0" 500 | }, 501 | "require-dev": { 502 | "php-coveralls/php-coveralls": "1.*", 503 | "phpunit/phpunit": "~4.0", 504 | "squizlabs/php_codesniffer": "~2.0", 505 | "symfony/process": "~2.8" 506 | }, 507 | "type": "library", 508 | "extra": { 509 | "branch-alias": { 510 | "dev-master": "2.0-dev" 511 | } 512 | }, 513 | "autoload": { 514 | "psr-4": { 515 | "Stripe\\": "lib/" 516 | } 517 | }, 518 | "notification-url": "https://packagist.org/downloads/", 519 | "license": [ 520 | "MIT" 521 | ], 522 | "authors": [ 523 | { 524 | "name": "Stripe and contributors", 525 | "homepage": "https://github.com/stripe/stripe-php/contributors" 526 | } 527 | ], 528 | "description": "Stripe PHP Library", 529 | "homepage": "https://stripe.com/", 530 | "keywords": [ 531 | "api", 532 | "payment processing", 533 | "stripe" 534 | ], 535 | "time": "2019-06-27T23:24:51+00:00" 536 | }, 537 | { 538 | "name": "symfony/polyfill-ctype", 539 | "version": "v1.11.0", 540 | "source": { 541 | "type": "git", 542 | "url": "https://github.com/symfony/polyfill-ctype.git", 543 | "reference": "82ebae02209c21113908c229e9883c419720738a" 544 | }, 545 | "dist": { 546 | "type": "zip", 547 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", 548 | "reference": "82ebae02209c21113908c229e9883c419720738a", 549 | "shasum": "" 550 | }, 551 | "require": { 552 | "php": ">=5.3.3" 553 | }, 554 | "suggest": { 555 | "ext-ctype": "For best performance" 556 | }, 557 | "type": "library", 558 | "extra": { 559 | "branch-alias": { 560 | "dev-master": "1.11-dev" 561 | } 562 | }, 563 | "autoload": { 564 | "psr-4": { 565 | "Symfony\\Polyfill\\Ctype\\": "" 566 | }, 567 | "files": [ 568 | "bootstrap.php" 569 | ] 570 | }, 571 | "notification-url": "https://packagist.org/downloads/", 572 | "license": [ 573 | "MIT" 574 | ], 575 | "authors": [ 576 | { 577 | "name": "Symfony Community", 578 | "homepage": "https://symfony.com/contributors" 579 | }, 580 | { 581 | "name": "Gert de Pagter", 582 | "email": "BackEndTea@gmail.com" 583 | } 584 | ], 585 | "description": "Symfony polyfill for ctype functions", 586 | "homepage": "https://symfony.com", 587 | "keywords": [ 588 | "compatibility", 589 | "ctype", 590 | "polyfill", 591 | "portable" 592 | ], 593 | "time": "2019-02-06T07:57:58+00:00" 594 | }, 595 | { 596 | "name": "vlucas/phpdotenv", 597 | "version": "v3.4.0", 598 | "source": { 599 | "type": "git", 600 | "url": "https://github.com/vlucas/phpdotenv.git", 601 | "reference": "5084b23845c24dbff8ac6c204290c341e4776c92" 602 | }, 603 | "dist": { 604 | "type": "zip", 605 | "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/5084b23845c24dbff8ac6c204290c341e4776c92", 606 | "reference": "5084b23845c24dbff8ac6c204290c341e4776c92", 607 | "shasum": "" 608 | }, 609 | "require": { 610 | "php": "^5.4 || ^7.0", 611 | "phpoption/phpoption": "^1.5", 612 | "symfony/polyfill-ctype": "^1.9" 613 | }, 614 | "require-dev": { 615 | "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0" 616 | }, 617 | "type": "library", 618 | "extra": { 619 | "branch-alias": { 620 | "dev-master": "3.4-dev" 621 | } 622 | }, 623 | "autoload": { 624 | "psr-4": { 625 | "Dotenv\\": "src/" 626 | } 627 | }, 628 | "notification-url": "https://packagist.org/downloads/", 629 | "license": [ 630 | "BSD-3-Clause" 631 | ], 632 | "authors": [ 633 | { 634 | "name": "Vance Lucas", 635 | "email": "vance@vancelucas.com", 636 | "homepage": "http://www.vancelucas.com" 637 | } 638 | ], 639 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 640 | "keywords": [ 641 | "dotenv", 642 | "env", 643 | "environment" 644 | ], 645 | "time": "2019-06-15T22:40:20+00:00" 646 | } 647 | ], 648 | "packages-dev": [], 649 | "aliases": [], 650 | "minimum-stability": "stable", 651 | "stability-flags": [], 652 | "prefer-stable": false, 653 | "prefer-lowest": false, 654 | "platform": [], 655 | "platform-dev": [] 656 | } 657 | -------------------------------------------------------------------------------- /server/php/config.php: -------------------------------------------------------------------------------- 1 | load(); 9 | 10 | 11 | require './config.php'; 12 | 13 | $app = new \Slim\App; 14 | 15 | // Instantiate the logger as a dependency 16 | $container = $app->getContainer(); 17 | $container['logger'] = function ($c) { 18 | $settings = $c->get('settings')['logger']; 19 | $logger = new Monolog\Logger($settings['name']); 20 | $logger->pushProcessor(new Monolog\Processor\UidProcessor()); 21 | $logger->pushHandler(new Monolog\Handler\StreamHandler(__DIR__ . '/logs/app.log', \Monolog\Logger::DEBUG)); 22 | return $logger; 23 | }; 24 | $app->add(function ($request, $response, $next) { 25 | Stripe::setApiKey(getenv('STRIPE_SECRET_KEY')); 26 | return $next($request, $response); 27 | }); 28 | 29 | $app->get('/', function (Request $request, Response $response, array $args) { 30 | // Display checkout page 31 | return $response->write(file_get_contents('../../client/index.html')); 32 | }); 33 | 34 | $app->get('/public-key', function (Request $request, Response $response, array $args) { 35 | $pub_key = getenv('STRIPE_PUBLISHABLE_KEY'); 36 | 37 | // Send publishable key details to client 38 | return $response->withJson(array('publicKey' => $pub_key)); 39 | }); 40 | 41 | $app->post('/create-customer', function (Request $request, Response $response, array $args) { 42 | $price_id = getenv('SUBSCRIPTION_PRICE_ID'); 43 | $body = json_decode($request->getBody()); 44 | 45 | # This creates a new Customer and attaches the PaymentMethod in one API call. 46 | # At this point, associate the ID of the Customer object with your 47 | # own internal representation of a customer, if you have one. 48 | $customer = \Stripe\Customer::create([ 49 | "payment_method" => $body->payment_method, 50 | "email" => $body->email, 51 | "invoice_settings" => [ 52 | "default_payment_method" => $body->payment_method 53 | ] 54 | ]); 55 | 56 | $subscription = \Stripe\Subscription::create([ 57 | "customer" => $customer['id'], 58 | "items" => [ 59 | [ 60 | "price" => $price_id, 61 | ], 62 | ], 63 | "expand" => ['latest_invoice.payment_intent'] 64 | ]); 65 | 66 | 67 | return $response->withJson($subscription); 68 | }); 69 | 70 | $app->post('/subscription', function (Request $request, Response $response, array $args) { 71 | $body = json_decode($request->getBody()); 72 | 73 | $subscription = \Stripe\Subscription::retrieve($body->subscriptionId); 74 | 75 | 76 | return $response->withJson($subscription); 77 | }); 78 | 79 | 80 | $app->post('/webhook', function(Request $request, Response $response) { 81 | $logger = $this->get('logger'); 82 | $event = $request->getParsedBody(); 83 | // Parse the message body (and check the signature if possible) 84 | $webhookSecret = getenv('STRIPE_WEBHOOK_SECRET'); 85 | if ($webhookSecret) { 86 | try { 87 | $event = \Stripe\Webhook::constructEvent( 88 | $request->getBody(), 89 | $request->getHeaderLine('stripe-signature'), 90 | $webhookSecret 91 | ); 92 | } catch (\Exception $e) { 93 | return $response->withJson([ 'error' => $e->getMessage() ])->withStatus(403); 94 | } 95 | } else { 96 | $event = $request->getParsedBody(); 97 | } 98 | $type = $event['type']; 99 | $object = $event['data']['object']; 100 | 101 | // Handle the event 102 | // Review important events for Billing webhooks 103 | // https://stripe.com/docs/billing/webhooks 104 | // Remove comment to see the various objects sent for this sample 105 | switch ($type) { 106 | case 'customer.created': 107 | $logger->info('🔔 Webhook received! ' . $object); 108 | break; 109 | case 'customer.updated': 110 | $logger->info('🔔 Webhook received! ' . $object); 111 | break; 112 | case 'invoice.upcoming': 113 | $logger->info('🔔 Webhook received! ' . $object); 114 | break; 115 | case 'invoice.created': 116 | $logger->info('🔔 Webhook received! ' . $object); 117 | break; 118 | case 'invoice.finalized': 119 | $logger->info('🔔 Webhook received! ' . $object); 120 | break; 121 | case 'invoice.payment_succeeded': 122 | $logger->info('🔔 Webhook received! ' . $object); 123 | break; 124 | case 'invoice.payment_failed': 125 | $logger->info('🔔 Webhook received! ' . $object); 126 | break; 127 | case 'customer.subscription.created': 128 | $logger->info('🔔 Webhook received! ' . $object); 129 | break; 130 | // ... handle other event types 131 | default: 132 | // Unexpected event type 133 | return $response->withStatus(400); 134 | } 135 | 136 | return $response->withJson([ 'status' => 'success' ])->withStatus(200); 137 | }); 138 | 139 | $app->run(); 140 | -------------------------------------------------------------------------------- /server/python/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | ## Requirements 4 | 5 | - Python 3 6 | - [Configured .env file](../README.md) 7 | 8 | ## How to run 9 | 10 | 1. Create and activate a new virtual environment 11 | 12 | ``` 13 | python3 -m venv /path/to/new/virtual/environment 14 | source /path/to/new/virtual/environment/bin/activate 15 | ``` 16 | 17 | 2. Install dependencies 18 | 19 | ``` 20 | pip install -r requirements.txt 21 | ``` 22 | 23 | 3. Export and run the application 24 | 25 | ``` 26 | export FLASK_APP=server.py 27 | python3 -m flask run --port=4242 28 | ``` 29 | 30 | 4. Go to `localhost:4242` in your browser to see the demo 31 | -------------------------------------------------------------------------------- /server/python/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.3.9 2 | chardet==3.0.4 3 | Click==7.0 4 | Flask==1.0.3 5 | idna==2.8 6 | itsdangerous==1.1.0 7 | Jinja2==2.10.1 8 | MarkupSafe==1.1.1 9 | python-dotenv==0.10.3 10 | requests==2.22.0 11 | stripe==2.32.1 12 | toml==0.9.6 13 | urllib3==1.25.3 14 | Werkzeug==0.15.4 15 | -------------------------------------------------------------------------------- /server/python/server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3.6 2 | 3 | """ 4 | server.py 5 | Stripe Recipe. 6 | Python 3.6 or newer required. 7 | """ 8 | 9 | import stripe 10 | import json 11 | import os 12 | 13 | from flask import Flask, render_template, jsonify, request, send_from_directory 14 | from dotenv import load_dotenv, find_dotenv 15 | 16 | # Setup Stripe python client library 17 | load_dotenv(find_dotenv()) 18 | stripe.api_key = os.getenv('STRIPE_SECRET_KEY') 19 | stripe.api_version = os.getenv('STRIPE_API_VERSION') 20 | 21 | static_dir = str(os.path.abspath(os.path.join( 22 | __file__, "..", os.getenv("STATIC_DIR")))) 23 | app = Flask(__name__, static_folder=static_dir, 24 | static_url_path="", template_folder=static_dir) 25 | 26 | 27 | @app.route('/', methods=['GET']) 28 | def get_index(): 29 | return render_template('index.html') 30 | 31 | 32 | @app.route('/public-key', methods=['GET']) 33 | def get_public_key(): 34 | return jsonify(publicKey=os.getenv('STRIPE_PUBLISHABLE_KEY')) 35 | 36 | 37 | @app.route('/create-customer', methods=['POST']) 38 | def create_customer(): 39 | # Reads application/json and returns a response 40 | data = json.loads(request.data) 41 | paymentMethod = data['payment_method'] 42 | print(paymentMethod) 43 | try: 44 | # This creates a new Customer and attaches the PaymentMethod in one API call. 45 | customer = stripe.Customer.create( 46 | payment_method=paymentMethod, 47 | email=data['email'], 48 | invoice_settings={ 49 | 'default_payment_method': paymentMethod 50 | } 51 | ) 52 | # At this point, associate the ID of the Customer object with your 53 | # own internal representation of a customer, if you have one. 54 | print(customer) 55 | 56 | # Subscribe the user to the subscription created 57 | subscription = stripe.Subscription.create( 58 | customer=customer.id, 59 | items=[ 60 | { 61 | "price": os.getenv("SUBSCRIPTION_PRICE_ID"), 62 | }, 63 | ], 64 | expand=["latest_invoice.payment_intent"] 65 | ) 66 | return jsonify(subscription) 67 | except Exception as e: 68 | return jsonify(error=str(e)), 403 69 | 70 | 71 | @app.route('/subscription', methods=['POST']) 72 | def getSubscription(): 73 | # Reads application/json and returns a response 74 | data = json.loads(request.data) 75 | try: 76 | subscription = stripe.Subscription.retrieve(data['subscriptionId']) 77 | return jsonify(subscription) 78 | except Exception as e: 79 | return jsonify(error=str(e)), 403 80 | 81 | 82 | @app.route('/webhook', methods=['POST']) 83 | def webhook_received(): 84 | # You can use webhooks to receive information about asynchronous payment events. 85 | # For more about our webhook events check out https://stripe.com/docs/webhooks. 86 | webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET') 87 | request_data = json.loads(request.data) 88 | 89 | if webhook_secret: 90 | # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured. 91 | signature = request.headers.get('stripe-signature') 92 | try: 93 | event = stripe.Webhook.construct_event( 94 | payload=request.data, sig_header=signature, secret=webhook_secret) 95 | data = event['data'] 96 | except Exception as e: 97 | return e 98 | # Get the type of webhook event sent - used to check the status of PaymentIntents. 99 | event_type = event['type'] 100 | else: 101 | data = request_data['data'] 102 | event_type = request_data['type'] 103 | 104 | data_object = data['object'] 105 | 106 | if event_type == 'customer.created': 107 | print(data) 108 | 109 | if event_type == 'customer.updated': 110 | print(data) 111 | 112 | if event_type == 'invoice.upcoming': 113 | print(data) 114 | 115 | if event_type == 'invoice.created': 116 | print(data) 117 | 118 | if event_type == 'invoice.finalized': 119 | print(data) 120 | 121 | if event_type == 'invoice.payment_succeeded': 122 | print(data) 123 | 124 | if event_type == 'invoice.payment_failed': 125 | print(data) 126 | 127 | if event_type == 'customer.subscription.created': 128 | print(data) 129 | 130 | return jsonify({'status': 'success'}) 131 | 132 | 133 | if __name__ == '__main__': 134 | app.run(port=4242) 135 | -------------------------------------------------------------------------------- /server/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | 3 | gem 'dotenv' 4 | gem 'json' 5 | gem 'sinatra' 6 | gem 'stripe' 7 | -------------------------------------------------------------------------------- /server/ruby/README.md: -------------------------------------------------------------------------------- 1 | # Stripe Billing charging for subscriptions 2 | 3 | A [Sinatra](http://sinatrarb.com/) implementation. 4 | 5 | ## Requirements 6 | 7 | - Ruby v2.4.5+ 8 | - [Configured .env file](../README.md) 9 | 10 | ## How to run 11 | 12 | 1. Install dependencies 13 | 14 | ``` 15 | bundle install 16 | ``` 17 | 18 | 2. Run the application 19 | 20 | ``` 21 | ruby server.rb 22 | ``` 23 | 24 | 3. Go to `localhost:4242` in your browser to see the demo 25 | -------------------------------------------------------------------------------- /server/ruby/server.rb: -------------------------------------------------------------------------------- 1 | require 'stripe' 2 | require 'sinatra' 3 | require "sinatra/reloader" if development? 4 | require 'dotenv' 5 | 6 | # Replace if using a different env file or config 7 | Dotenv.load 8 | Stripe.api_key = ENV['STRIPE_SECRET_KEY'] 9 | 10 | 11 | set :static, true 12 | set :public_folder, File.join(File.dirname(__FILE__), ENV['STATIC_DIR']) 13 | set :port, 4242 14 | 15 | get '/' do 16 | content_type 'text/html' 17 | send_file File.join(settings.public_folder, 'index.html') 18 | end 19 | 20 | get '/public-key' do 21 | content_type 'application/json' 22 | 23 | { 24 | 'publicKey': ENV['STRIPE_PUBLISHABLE_KEY'] 25 | }.to_json 26 | end 27 | 28 | post '/create-customer' do 29 | content_type 'application/json' 30 | data = JSON.parse request.body.read 31 | 32 | # This creates a new Customer and attaches the PaymentMethod in one API call. 33 | # At this point, associate the ID of the Customer object with your 34 | # own internal representation of a customer, if you have one. 35 | customer = Stripe::Customer.create( 36 | payment_method: data['payment_method'], 37 | email: data['email'], 38 | invoice_settings: { 39 | default_payment_method: data['payment_method'] 40 | } 41 | ) 42 | 43 | subscription = Stripe::Subscription.create( 44 | customer: customer.id, 45 | items: [ 46 | { 47 | price: ENV['SUBSCRIPTION_PRICE_ID'] 48 | } 49 | ], 50 | expand: ['latest_invoice.payment_intent'] 51 | ) 52 | 53 | subscription.to_json 54 | end 55 | 56 | post '/subscription' do 57 | content_type 'application/json' 58 | data = JSON.parse request.body.read 59 | 60 | subscription = Stripe::Subscription.retrieve(data['subscriptionId']) 61 | 62 | subscription.to_json 63 | end 64 | 65 | post '/webhook' do 66 | # You can use webhooks to receive information about asynchronous payment events. 67 | # For more about our webhook events check out https://stripe.com/docs/webhooks. 68 | webhook_secret = ENV['STRIPE_WEBHOOK_SECRET'] 69 | payload = request.body.read 70 | if !webhook_secret.empty? 71 | # Retrieve the event by verifying the signature using the raw body and secret if webhook signing is configured. 72 | sig_header = request.env['HTTP_STRIPE_SIGNATURE'] 73 | event = nil 74 | 75 | begin 76 | event = Stripe::Webhook.construct_event( 77 | payload, sig_header, webhook_secret 78 | ) 79 | rescue JSON::ParserError => e 80 | # Invalid payload 81 | status 400 82 | return 83 | rescue Stripe::SignatureVerificationError => e 84 | # Invalid signature 85 | puts "⚠️ Webhook signature verification failed." 86 | status 400 87 | return 88 | end 89 | else 90 | data = JSON.parse(payload, symbolize_names: true) 91 | event = Stripe::Event.construct_from(data) 92 | end 93 | # Get the type of webhook event sent - used to check the status of PaymentIntents. 94 | event_type = event['type'] 95 | data = event['data'] 96 | data_object = data['object'] 97 | 98 | if event_type == 'customer.created' 99 | # puts data_object 100 | end 101 | 102 | if event_type == 'customer.updated' 103 | # puts data_object 104 | end 105 | 106 | if event_type == 'invoice.upcoming' 107 | # puts data_object 108 | end 109 | 110 | if event_type == 'invoice.created' 111 | # puts data_object 112 | end 113 | 114 | if event_type == 'invoice.finalized' 115 | # puts data_object 116 | end 117 | 118 | if event_type == 'invoice.payment_succeeded' 119 | # puts data_object 120 | end 121 | 122 | if event_type == 'invoice.payment_failed' 123 | # puts data_object 124 | end 125 | 126 | if event_type == 'customer.subscription.created' 127 | # puts data_object 128 | end 129 | 130 | content_type 'application/json' 131 | { 132 | status: 'success' 133 | }.to_json 134 | 135 | end 136 | --------------------------------------------------------------------------------