Skip to content

Commit d47e926

Browse files
authored
Stripe SCA Regulations Implementation (codesandbox#2447)
* Initial implementation * Add cancel subscription text * Use more robust type checking and load stripe when needed * Fix type * Fix Stripe props
1 parent 51361dc commit d47e926

File tree

9 files changed

+139
-34
lines changed

9 files changed

+139
-34
lines changed

packages/app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
"react-show": "^3.0.4",
187187
"react-split-pane": "^0.1.87",
188188
"react-spring": "^8.0.25",
189-
"react-stripe-elements": "^3.0.0",
189+
"react-stripe-elements": "^5.0.0",
190190
"react-tagsinput": "^3.19.0",
191191
"react-use": "^9.7.2",
192192
"react-virtualized": "^9.19.1",
@@ -235,8 +235,10 @@
235235
"@types/react": "^16.8.12",
236236
"@types/react-dom": "^16.8.3",
237237
"@types/react-icons": "2.2.7",
238+
"@types/react-stripe-elements": "^1.3.2",
238239
"@types/resolve": "^0.0.8",
239240
"@types/socket.io-client": "^1.4.32",
241+
"@types/stripe-v3": "^3.1.7",
240242
"@types/styled-components": "^4.1.13",
241243
"acorn-dynamic-import": "^4.0.0",
242244
"babel-jest": "^24.8.0",

packages/app/src/app/components/SubscribeForm/CheckoutForm/index.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,17 @@
11
import React from 'react';
2-
import { injectStripe, CardElement } from 'react-stripe-elements';
2+
import {
3+
injectStripe,
4+
CardElement,
5+
ReactStripeElements,
6+
} from 'react-stripe-elements';
37
import { Button } from '@codesandbox/common/lib/components/Button';
48
import { logError } from '@codesandbox/common/lib/utils/analytics';
59

610
import { CardContainer, StripeInput, ErrorText, Label } from './elements';
711

8-
interface IStripe {
9-
createToken: (params: {
10-
name: string;
11-
}) => Promise<{
12-
token: { id: string };
13-
error?: Error;
14-
}>;
15-
}
16-
1712
interface Props {
1813
name: string;
19-
stripe: IStripe;
14+
stripe: ReactStripeElements.StripeProps;
2015
buttonName: string;
2116
loadingText: string;
2217
isLoading: boolean;

packages/app/src/app/overmind/effects/api/apiFactory.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable camelcase */
12
import axios, { AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios';
23
import { logError } from '@codesandbox/common/lib/utils/analytics';
34
import { values } from 'lodash-es';
@@ -46,20 +47,19 @@ export default (config: {
4647
: {};
4748

4849
const showError = error => {
49-
const errorMessage = getMessage(error);
50-
51-
config.onError(errorMessage);
52-
error.apiMessage = errorMessage; // eslint-disable-line no-param-reassign
50+
config.onError(error.message);
51+
error.apiMessage = error.message; // eslint-disable-line no-param-reassign
5352
};
5453

5554
const handleError = error => {
55+
const newError = convertError(error);
5656
try {
57-
showError(error);
57+
showError(newError);
5858
} catch (e) {
5959
console.error(e);
6060
}
6161

62-
throw error;
62+
throw newError;
6363
};
6464

6565
const api: Api = {
@@ -122,7 +122,7 @@ export default (config: {
122122
return api;
123123
};
124124

125-
function getMessage(error: AxiosError) {
125+
function convertError(error: AxiosError) {
126126
const response = error.response;
127127

128128
if (!response || response.status >= 500) {
@@ -140,13 +140,22 @@ function getMessage(error: AxiosError) {
140140
error.message = errors; // eslint-disable-line no-param-reassign
141141
}
142142
} else if (response.data.error) {
143-
error.message = response.data.error; // eslint-disable-line no-param-reassign
143+
const { error_code, message, ...data } = response.data.error as {
144+
message: string;
145+
error_code: string;
146+
[k: string]: any;
147+
};
148+
// @ts-ignore
149+
error.error_code = error_code; // eslint-disable-line no-param-reassign
150+
error.message = message; // eslint-disable-line no-param-reassign
151+
// @ts-ignore
152+
error.data = data; // eslint-disable-line no-param-reassign
144153
} else if (response.status === 413) {
145154
return 'File too large, upload limit is 5MB.';
146155
}
147156
}
148157

149-
return error.message;
158+
return error;
150159
}
151160

152161
export function handleResponse(

packages/app/src/app/overmind/effects/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export { default as zip } from './zip';
2424
export { default as codesandboxApi } from './codesandboxApi';
2525
export { default as themes } from './themes';
2626
export { default as executor } from './executor';
27+
export { default as stripe } from './stripe';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { STRIPE_API_KEY } from '@codesandbox/common/lib/utils/config';
2+
3+
function loadScript(path: string) {
4+
return new Promise(resolve => {
5+
if (typeof document !== 'undefined') {
6+
var script = document.createElement('script');
7+
script.onload = resolve;
8+
script.async = true;
9+
script.type = 'text/javascript';
10+
script.src = path;
11+
document.head.appendChild(script);
12+
}
13+
});
14+
}
15+
16+
let localStripeVar: stripe.Stripe;
17+
const getStripe = async (): Promise<stripe.Stripe> => {
18+
if (typeof Stripe === 'undefined') {
19+
await loadScript('https://js.stripe.com/v3/');
20+
}
21+
22+
if (!localStripeVar) {
23+
// @ts-ignore
24+
localStripeVar = Stripe(STRIPE_API_KEY);
25+
}
26+
27+
return localStripeVar;
28+
};
29+
30+
export default {
31+
handleCardPayment: async (paymentIntent: string) => {
32+
const stripe = await getStripe();
33+
34+
return stripe.handleCardPayment(paymentIntent);
35+
},
36+
};

packages/app/src/app/overmind/namespaces/patron/actions.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { AsyncAction, Action } from 'app/overmind';
22
import { withLoadApp } from 'app/overmind/factories';
33

4+
export enum StripeErrorCode {
5+
REQUIRES_ACTION = 'requires_action',
6+
}
7+
48
export const patronMounted: AsyncAction = withLoadApp();
59

610
export const priceChanged: Action<{ price: number }> = (
@@ -25,7 +29,21 @@ export const createSubscriptionClicked: AsyncAction<{
2529
);
2630
effects.notificationToast.error('Thank you very much for your support!');
2731
} catch (error) {
28-
state.patron.error = error.message;
32+
if (
33+
error.error_code &&
34+
error.error_code === StripeErrorCode.REQUIRES_ACTION
35+
) {
36+
try {
37+
await effects.stripe.handleCardPayment(error.data.client_secret);
38+
39+
state.user = await effects.api.getCurrentUser();
40+
state.patron.error = null;
41+
} catch (e) {
42+
state.patron.error = e.message;
43+
}
44+
} else {
45+
state.patron.error = error.message;
46+
}
2947
}
3048
state.patron.isUpdatingSubscription = false;
3149
};

packages/app/src/app/pages/Patron/PricingModal/PricingChoice/ChangeSubscription/elements.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,26 @@ export const StripeInput = styled(Input)`
2929
margin-bottom: 0.5rem;
3030
height: 32.8px;
3131
`;
32+
33+
export const Centered = styled.div`
34+
display: flex;
35+
flex-direction: row;
36+
justify-content: center;
37+
`;
38+
39+
export const CancelText = styled.button`
40+
transition: 0.3s ease color;
41+
background-color: transparent;
42+
outline: 0;
43+
border: 0;
44+
color: rgba(255, 255, 255, 0.4);
45+
text-align: center;
46+
cursor: pointer;
47+
text-decoration: underline;
48+
font-size: 0.875rem;
49+
margin-top: 1rem;
50+
51+
&:hover {
52+
color: rgba(255, 255, 255, 0.6);
53+
}
54+
`;

packages/app/src/app/pages/Patron/PricingModal/PricingChoice/ChangeSubscription/index.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { inject, hooksObserver } from 'app/componentConnectors';
33
import { format } from 'date-fns';
44
import { LinkButton } from 'app/components/LinkButton';
55

6-
import { SmallText, Buttons, StyledButton, StripeInput } from './elements';
6+
import {
7+
SmallText,
8+
Buttons,
9+
StyledButton,
10+
StripeInput,
11+
CancelText,
12+
Centered,
13+
} from './elements';
714

815
interface Props {
916
date: string;
@@ -43,22 +50,23 @@ function ChangeSubscriptionComponent({
4350

4451
let buttons = (
4552
<>
46-
<div style={{ margin: '1rem 5rem', marginTop: '2rem' }}>
53+
<div style={{ margin: '0 5rem', marginTop: '2rem' }}>
4754
<StripeInput
4855
onChange={e => setCoupon(e.target.value)}
4956
value={coupon}
5057
placeholder="Apply Coupon Code"
5158
/>
5259
</div>
53-
5460
<Buttons>
55-
<StyledButton onClick={() => cancelSubscription()} red>
56-
Cancel
57-
</StyledButton>
5861
<StyledButton onClick={() => updateSubscription({ coupon })}>
5962
Update
6063
</StyledButton>
6164
</Buttons>
65+
<Centered>
66+
<CancelText onClick={() => cancelSubscription()}>
67+
Cancel my subscription
68+
</CancelText>
69+
</Centered>
6270
</>
6371
);
6472

yarn.lock

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4069,6 +4069,14 @@
40694069
"@types/prop-types" "*"
40704070
"@types/react" "*"
40714071

4072+
"@types/react-stripe-elements@^1.3.2":
4073+
version "1.3.2"
4074+
resolved "https://registry.yarnpkg.com/@types/react-stripe-elements/-/react-stripe-elements-1.3.2.tgz#02ed6802b16366b4ebc6b85b8bd3e8befa553b79"
4075+
integrity sha512-b+H5HARxeVM4Qw07QKYaOwcNCwFEPpo0q7FDMmh7Wfh5ESSFD+SqFe0/AMuWi1mPIx2py8L+jIR/5C30jvDWDg==
4076+
dependencies:
4077+
"@types/react" "*"
4078+
"@types/stripe-v3" "*"
4079+
40724080
"@types/react@*", "@types/react@^16.8.12", "@types/react@^16.8.6":
40734081
version "16.9.2"
40744082
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.2.tgz#6d1765431a1ad1877979013906731aae373de268"
@@ -4127,6 +4135,11 @@
41274135
"@types/react" "*"
41284136
"@types/webpack-env" "*"
41294137

4138+
"@types/stripe-v3@*", "@types/stripe-v3@^3.1.7":
4139+
version "3.1.7"
4140+
resolved "https://registry.yarnpkg.com/@types/stripe-v3/-/stripe-v3-3.1.7.tgz#7ae64076e663af2852d23706deb503d3c5ce7e67"
4141+
integrity sha512-S0cfUfMna2FjLkpQOooyIZvx1OqQLXMoltzJFkDL5zAaXEWX0dsA8S6HKTj2j+dIJhKKw4xw1NE3esLYEeDk4Q==
4142+
41304143
"@types/styled-components@^4.1.13":
41314144
version "4.1.13"
41324145
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.1.13.tgz#7373e0a084dedf6982a35d5bc2ef07e023e18e2e"
@@ -7949,7 +7962,7 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
79497962
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
79507963

79517964
console-feed@CompuIves/console-feed#build2, console-feed@^2.8.5:
7952-
version "2.8.9"
7965+
version "2.8.8"
79537966
resolved "https://codeload.github.com/CompuIves/console-feed/tar.gz/42f10eb3063f0f26ee9745c4c9e4542cb5591f46"
79547967
dependencies:
79557968
emotion "^9.1.1"
@@ -21184,12 +21197,12 @@ react-spring@^8.0.25:
2118421197
"@babel/runtime" "^7.3.1"
2118521198
prop-types "^15.5.8"
2118621199

21187-
react-stripe-elements@^3.0.0:
21188-
version "3.0.0"
21189-
resolved "https://registry.yarnpkg.com/react-stripe-elements/-/react-stripe-elements-3.0.0.tgz#8b5e354e6e421b85f909329c9818a719b132410a"
21190-
integrity sha512-ouGHPmPhdg7KUTOFVuYIr0UWVgBjG5CqmOn9/y0vaJICryPB6llkiOGeUXKrac7fu1/vtPdn8t/4JKnbi8PT8g==
21200+
react-stripe-elements@^5.0.0:
21201+
version "5.0.0"
21202+
resolved "https://registry.yarnpkg.com/react-stripe-elements/-/react-stripe-elements-5.0.0.tgz#3a67717ddc9580c4dad9d45b672a9cf6c8a6dda4"
21203+
integrity sha512-zYYmlaROlkRV+Naz2abUZL4DnnQFzlSKc1PmAx7DKDi8PdFUEMiXk5kepPFlIfhwzq5fgoSptT2Y3Jri0p7cAQ==
2119121204
dependencies:
21192-
prop-types "^15.5.10"
21205+
prop-types "15.7.2"
2119321206

2119421207
react-style-proptype@^3.0.0:
2119521208
version "3.2.1"

0 commit comments

Comments
 (0)