├── .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 |
21 |
22 |
$14.00
23 | Subscribe to the 3 photo plan
24 |
25 |
57 |
58 |
Your subscription is
59 |
62 |
63 |
64 |
67 |
68 |
69 |
70 |
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 |
--------------------------------------------------------------------------------