@blocklet/launcher-workflow 2.1.108 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/es/assets/images/blocklet-server.svg +10 -0
  2. package/es/assets/images/check-reverse.svg +5 -0
  3. package/es/assets/images/check.svg +5 -0
  4. package/es/assets/images/checkbox-checked.svg +5 -0
  5. package/es/assets/images/checkbox-unchecked.svg +3 -0
  6. package/es/assets/images/next.svg +8 -0
  7. package/es/assets/images/prev.svg +8 -0
  8. package/es/assets/images/space.svg +9 -0
  9. package/es/assets/images/uncheck-reverse.svg +4 -0
  10. package/es/assets/images/uncheck.svg +4 -0
  11. package/es/checkout.js +573 -0
  12. package/es/components/agreement.js +5 -0
  13. package/es/components/checkbox.js +94 -0
  14. package/es/components/confirm.js +95 -0
  15. package/es/components/in-progress-session.js +96 -0
  16. package/es/components/launch-dedicated.js +290 -0
  17. package/es/components/launch-serverless.js +113 -0
  18. package/es/components/layout/body.js +41 -0
  19. package/es/components/layout/footer.js +21 -0
  20. package/es/components/layout/header.js +25 -0
  21. package/es/components/layout/index.js +24 -0
  22. package/es/components/plan.js +445 -0
  23. package/es/contexts/locale.js +2 -0
  24. package/es/contexts/request.js +71 -0
  25. package/es/contexts/session.js +13 -0
  26. package/es/contexts/workflow.js +64 -0
  27. package/es/hooks/query.js +4 -0
  28. package/es/launch.js +36 -0
  29. package/es/locales/en.js +135 -0
  30. package/es/locales/index.js +8 -0
  31. package/es/locales/zh.js +134 -0
  32. package/es/purchase.js +631 -0
  33. package/es/util.js +165 -0
  34. package/lib/checkout.js +5 -5
  35. package/lib/components/checkbox.js +2 -2
  36. package/lib/components/in-progress-session.js +4 -4
  37. package/lib/components/launch-dedicated.js +3 -3
  38. package/lib/components/launch-serverless.js +1 -1
  39. package/lib/components/layout/body.js +2 -2
  40. package/lib/components/layout/footer.js +2 -2
  41. package/lib/components/layout/index.js +2 -2
  42. package/lib/components/plan.js +6 -6
  43. package/lib/contexts/request.js +2 -2
  44. package/lib/locales/en.js +7 -2
  45. package/lib/locales/index.js +15 -9
  46. package/lib/locales/zh.js +7 -2
  47. package/lib/purchase.js +4 -4
  48. package/package.json +32 -7
package/es/purchase.js ADDED
@@ -0,0 +1,631 @@
1
+ /* eslint-disable react-hooks/exhaustive-deps */
2
+ import Connect, { useSecurity } from '@arcblock/did-connect/lib/Connect';
3
+ import Button from '@arcblock/ux/lib/Button';
4
+ import Center from '@arcblock/ux/lib/Center';
5
+ import CompactLayout from '@blocklet/launcher-layout/lib/compact-layout';
6
+ import { NFT_TYPE_SERVERLESS } from '@blocklet/launcher-util/es/constant';
7
+ import useMobile from '@blocklet/launcher-ux/lib/use-mobile';
8
+ import styled from '@emotion/styled';
9
+ import CircularProgress from '@mui/material/CircularProgress';
10
+ import Tooltip from '@mui/material/Tooltip';
11
+ import { Splide, SplideSlide, SplideTrack } from '@splidejs/react-splide';
12
+ import { Grid } from '@splidejs/splide-extension-grid';
13
+ import get from 'lodash.get';
14
+ import PropTypes from 'prop-types';
15
+ import React, { useEffect } from 'react';
16
+ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
17
+ import useMeasure from 'react-use/lib/useMeasure';
18
+ import useSetState from 'react-use/lib/useSetState';
19
+ import joinURL from 'url-join';
20
+ import '@splidejs/splide/dist/css/splide.min.css';
21
+ import ResultMessage from '@blocklet/launcher-layout/lib/launch-result-message';
22
+ import formatError from '@blocklet/launcher-util/es/format-error';
23
+ var NextIcon = function NextIcon(props) {
24
+ return /*#__PURE__*/_jsx("svg", {
25
+ ...props,
26
+ children: /*#__PURE__*/_jsx("path", {
27
+ d: "m15.5.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4L15.5.912z"
28
+ })
29
+ });
30
+ };
31
+ NextIcon.defaultProps = {
32
+ xmlns: "http://www.w3.org/2000/svg",
33
+ width: "40",
34
+ height: "40"
35
+ };
36
+ var PrevIcon = function PrevIcon(props) {
37
+ return /*#__PURE__*/_jsx("svg", {
38
+ ...props,
39
+ children: /*#__PURE__*/_jsx("path", {
40
+ d: "m15.5.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4L15.5.912z"
41
+ })
42
+ });
43
+ };
44
+ PrevIcon.defaultProps = {
45
+ xmlns: "http://www.w3.org/2000/svg",
46
+ width: "40",
47
+ height: "40"
48
+ };
49
+ import InProgressSession from './components/in-progress-session';
50
+ import Layout from './components/layout';
51
+ import Body from './components/layout/body';
52
+ import Footer from './components/layout/footer';
53
+ import Header from './components/layout/header';
54
+ import Plan from './components/plan';
55
+ import { useLocaleContext } from './contexts/locale';
56
+ import useRequest from './contexts/request';
57
+ import { useSessionContext } from './contexts/session';
58
+ import { useWorkflowContext } from './contexts/workflow';
59
+ import { launchSession } from './util';
60
+ import { jsx as _jsx } from "react/jsx-runtime";
61
+ import { jsxs as _jsxs } from "react/jsx-runtime";
62
+ import { Fragment as _Fragment } from "react/jsx-runtime";
63
+ const SLIDE_WIDTH = 300;
64
+ const SLIDE_MIN_WIDTH = 280;
65
+ const getSlideConfig = (maxLength, planLength) => {
66
+ const factor = Math.min(maxLength, planLength);
67
+ const width = factor < 3 ? SLIDE_MIN_WIDTH : SLIDE_WIDTH;
68
+ return {
69
+ perPage: factor,
70
+ width: `${factor * width}px`,
71
+ gap: '24px'
72
+ };
73
+ };
74
+ function PurchasePage({
75
+ disableBack
76
+ }) {
77
+ const {
78
+ t,
79
+ locale
80
+ } = useLocaleContext();
81
+ const {
82
+ create,
83
+ api: launchSessionAPI
84
+ } = useRequest();
85
+ const {
86
+ isMobile
87
+ } = useMobile();
88
+ const {
89
+ decrypt
90
+ } = useSecurity();
91
+ const navigate = useNavigate();
92
+ const {
93
+ session,
94
+ storage,
95
+ events
96
+ } = useSessionContext();
97
+ const [params, setParams] = useSearchParams();
98
+ const {
99
+ routerPrefix,
100
+ embed,
101
+ didPayPrefix,
102
+ isPurchaseOnly
103
+ } = useWorkflowContext();
104
+ const [planListRef, {
105
+ width
106
+ }] = useMeasure();
107
+ const [state, setState] = useSetState({
108
+ loading: true,
109
+ error: '',
110
+ plans: [],
111
+ paymentMethods: [],
112
+ paymentMethod: params.get('paymentMethod'),
113
+ sessionId: params.get('sessionId'),
114
+ planId: params.get('planId'),
115
+ showRedeem: false,
116
+ showMorePrompt: false,
117
+ arrows: false,
118
+ perPage: 100,
119
+ width: `${SLIDE_WIDTH}px`,
120
+ gap: '16px'
121
+ });
122
+ const api = create({
123
+ baseURL: didPayPrefix
124
+ });
125
+ const selectPlan = launch => {
126
+ if (!state.plans.length) {
127
+ return;
128
+ }
129
+ if (launch && launch.planId && state.plans.find(x => x._id === launch.planId)) {
130
+ setState({
131
+ planId: launch.planId
132
+ });
133
+ }
134
+ const recommended = state.plans.find(x => !!x.isRecommended);
135
+ if (recommended) {
136
+ setState({
137
+ planId: recommended._id
138
+ });
139
+ } else {
140
+ setState({
141
+ planId: state.plans[0]._id
142
+ });
143
+ }
144
+ };
145
+
146
+ // 用户退出的时候自动重新创建启动会话
147
+ useEffect(() => {
148
+ if (isPurchaseOnly) {
149
+ return;
150
+ }
151
+ events.on('logout', () => {
152
+ const metaUrl = params.get('blocklet_meta_url');
153
+ launchSession.create(launchSessionAPI, routerPrefix, metaUrl, (err, launch) => {
154
+ setState({
155
+ sessionId: launch._id
156
+ });
157
+ selectPlan(launch);
158
+ });
159
+ });
160
+ }, []);
161
+
162
+ // 创建启动会话或者加载现有的会话
163
+ useEffect(() => {
164
+ Promise.all([api.get('/plans'), api.get('/payment-methods')]).then(([{
165
+ data
166
+ }, {
167
+ data: {
168
+ data: paymentMethods
169
+ }
170
+ }]) => {
171
+ if (data.length === 0 || paymentMethods.length === 0) {
172
+ setState({
173
+ loading: false,
174
+ error: t('purchase.noProducts')
175
+ });
176
+ return;
177
+ }
178
+ let plans = (data || []).sort((a, b) => b.weight - a.weight);
179
+ const {
180
+ components = []
181
+ } = get(window, 'blockletMeta', {}) || {};
182
+ const supported = components.every(x => get(x, 'meta.capabilities.serverless', true));
183
+ plans.forEach(x => {
184
+ if (get(x, 'extra.type') === 'serverless' && !supported) {
185
+ x.available = false;
186
+ x.reason = t('purchase.serverlessNotSupported');
187
+ } else {
188
+ x.available = true;
189
+ }
190
+ });
191
+ plans = plans.sort((a, b) => {
192
+ if (b.available && !a.available) {
193
+ return 1;
194
+ }
195
+ if (!b.available && a.available) {
196
+ return -1;
197
+ }
198
+ return 0;
199
+ });
200
+ setState({
201
+ plans,
202
+ paymentMethods,
203
+ paymentMethod: paymentMethods[0]._id,
204
+ loading: false
205
+ });
206
+ if (isPurchaseOnly) {
207
+ selectPlan();
208
+ return;
209
+ }
210
+ const metaUrl = params.get('blocklet_meta_url');
211
+ if (params.get('sessionId')) {
212
+ launchSession.load(launchSessionAPI, routerPrefix, params.get('sessionId'), metaUrl, (err, launch) => {
213
+ selectPlan(launch);
214
+ });
215
+ } else if (metaUrl) {
216
+ launchSession.create(launchSessionAPI, routerPrefix, metaUrl, (err, launch) => {
217
+ setState({
218
+ sessionId: launch._id
219
+ });
220
+ selectPlan(launch);
221
+ });
222
+ }
223
+ }).catch(err => {
224
+ setState({
225
+ error: formatError(err)
226
+ });
227
+ });
228
+ }, []);
229
+ useEffect(() => {
230
+ if (!state.sessionId) {
231
+ return;
232
+ }
233
+ if (state.sessionId !== params.get('sessionId')) {
234
+ params.set('sessionId', state.sessionId);
235
+ setParams(params, {
236
+ replace: true
237
+ });
238
+ }
239
+ }, [state.sessionId]);
240
+ useEffect(() => {
241
+ if (!state.plans?.length || typeof state.perPage === 'undefined') {
242
+ return;
243
+ }
244
+ const index = state.plans.findIndex(x => !!x.isRecommended && x.available);
245
+ const pageIndex = Math.floor(index / state.perPage);
246
+
247
+ // 如果推荐 Plan 不在第一页,则将推荐 Plan 放在第一个
248
+ if (pageIndex >= 1) {
249
+ const recommend = state.plans[index];
250
+ const copy = [...state.plans];
251
+ copy.splice(index, 1);
252
+ copy.unshift(recommend);
253
+ setState({
254
+ plans: copy
255
+ });
256
+ }
257
+ }, [state.perPage, state.plans]);
258
+ useEffect(() => {
259
+ if (!state.plans?.length) {
260
+ return;
261
+ }
262
+ if (Number.isNaN(width)) {
263
+ return;
264
+ }
265
+
266
+ // 最小得有1个
267
+ const tmp = Math.max(Math.floor(width / SLIDE_WIDTH) - 1, 1);
268
+ setState(getSlideConfig(tmp, state.plans.length));
269
+ }, [width, state.plans, isMobile]);
270
+ const handleSelect = planId => {
271
+ if (state.planId === planId) {
272
+ return;
273
+ }
274
+ if (state.plans.find(x => x._id === planId)?.available === false) {
275
+ return;
276
+ }
277
+ setState({
278
+ planId
279
+ });
280
+ };
281
+ useEffect(() => {
282
+ if (!state.planId) {
283
+ return;
284
+ }
285
+ if (state.planId !== params.get('planId')) {
286
+ params.set('planId', state.planId);
287
+ setParams(params, {
288
+ replace: true
289
+ });
290
+ }
291
+ }, [state.planId]);
292
+ useEffect(() => {
293
+ if (!state.paymentMethod) {
294
+ return;
295
+ }
296
+ if (state.paymentMethod !== params.get('paymentMethod')) {
297
+ params.set('paymentMethod', state.paymentMethod);
298
+ setParams(params, {
299
+ replace: true
300
+ });
301
+ }
302
+ }, [state.paymentMethod]);
303
+ useEffect(() => {
304
+ if (isPurchaseOnly) {
305
+ return;
306
+ }
307
+ if (!state.planId || !state.sessionId) {
308
+ return;
309
+ }
310
+ const plan = state.plans.find(x => x._id === state.planId);
311
+ const type = get(plan, 'extra.type') || 'serverless';
312
+ launchSession.select(launchSessionAPI, routerPrefix, state.sessionId, type, state.planId);
313
+ }, [state.planId, state.sessionId]);
314
+ const productFeatures = [];
315
+ if (!state.loading && state.plans?.length > 0) {
316
+ // TODO: Pricing Table: 临时做法
317
+ try {
318
+ const {
319
+ features
320
+ } = state.plans[0];
321
+ if (features && features.length > 0) {
322
+ const tmp = JSON.parse(features[0].en);
323
+ tmp.forEach(item => {
324
+ productFeatures.push({
325
+ _id: item.featureId,
326
+ name: item.name,
327
+ type: item.type
328
+ });
329
+ });
330
+ }
331
+ } catch (error) {
332
+ console.error('parse product feature failed:', error);
333
+ }
334
+ }
335
+ const handleRedeem = () => {
336
+ setState({
337
+ showRedeem: true
338
+ });
339
+ };
340
+ const handleRedeemed = ({
341
+ loginToken,
342
+ nftId,
343
+ type,
344
+ sessionId
345
+ }) => {
346
+ if (type === NFT_TYPE_SERVERLESS) {
347
+ params.set('launchType', 'serverless');
348
+ }
349
+ if (loginToken) {
350
+ storage.setToken(decrypt(loginToken));
351
+ session.refresh();
352
+ }
353
+ if (sessionId) {
354
+ params.set('sessionId', sessionId);
355
+ }
356
+ navigate({
357
+ pathname: joinURL(routerPrefix, `/launch/${nftId}`),
358
+ search: params.toString()
359
+ });
360
+ };
361
+ const plan = state.planId && state.plans ? state.plans.find(x => x._id === state.planId) : null;
362
+ return /*#__PURE__*/_jsxs(Container, {
363
+ embed: embed,
364
+ children: [/*#__PURE__*/_jsx(Header, {
365
+ title: t('purchase.pageTitle'),
366
+ disableBack: disableBack
367
+ }), /*#__PURE__*/_jsx(Body, {
368
+ style: {
369
+ justifyContent: 'center'
370
+ },
371
+ children: /*#__PURE__*/_jsxs("div", {
372
+ style: {
373
+ flex: 1,
374
+ position: 'relative'
375
+ },
376
+ children: [state.error && /*#__PURE__*/_jsx(ResultMessage, {
377
+ variant: "error",
378
+ title: t('common.error'),
379
+ subTitle: state.error
380
+ }), !state.error && /*#__PURE__*/_jsx("div", {
381
+ className: "container-inner",
382
+ children: /*#__PURE__*/_jsx(CompactLayout, {
383
+ bottom: /*#__PURE__*/_jsxs(Footer, {
384
+ className: "footer",
385
+ children: [embed && isPurchaseOnly === false && /*#__PURE__*/_jsxs("div", {
386
+ className: "footer-left",
387
+ children: [/*#__PURE__*/_jsx(Button, {
388
+ onClick: handleRedeem,
389
+ className: "redeem-button",
390
+ variant: "outlined",
391
+ children: t('purchase.redeem')
392
+ }), /*#__PURE__*/_jsx(Link, {
393
+ className: "select-space",
394
+ to: joinURL(routerPrefix, `/purchase/select${window.location.search}`),
395
+ children: t('purchase.selectSpaceHint')
396
+ })]
397
+ }), /*#__PURE__*/_jsx(Button, {
398
+ component: Link,
399
+ disabled: state.loading || !state.planId,
400
+ to: joinURL(routerPrefix, `/purchase/checkout?${params.toString()}`),
401
+ className: "button-next",
402
+ variant: "contained",
403
+ children: plan ? t('purchase.hasPlan', {
404
+ name: plan.name[locale]
405
+ }) : t('purchase.noPlan')
406
+ })]
407
+ }),
408
+ children: /*#__PURE__*/_jsx("div", {
409
+ ref: planListRef,
410
+ className: "plan-list",
411
+ children: state.loading ? /*#__PURE__*/_jsx(Center, {
412
+ relative: "parent",
413
+ children: /*#__PURE__*/_jsx(CircularProgress, {})
414
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
415
+ children: [!isMobile && /*#__PURE__*/_jsx(Plan, {
416
+ toc: true,
417
+ productFeatures: productFeatures,
418
+ paymentMethods: state.paymentMethods,
419
+ paymentMethod: state.paymentMethod,
420
+ onChangeMethod: paymentMethod => setState({
421
+ paymentMethod
422
+ }),
423
+ className: "toc"
424
+ }), /*#__PURE__*/_jsxs(Splide, {
425
+ extensions: {
426
+ Grid
427
+ },
428
+ onPaginationMounted: splide => {
429
+ const pageCount = Math.ceil(state.plans.length / splide.options.perPage);
430
+ const showArrows = pageCount > 1;
431
+ setState({
432
+ arrows: showArrows
433
+ });
434
+ if (showArrows) {
435
+ setTimeout(() => {
436
+ setState({
437
+ showMorePrompt: true
438
+ });
439
+ // 3s 后隐藏
440
+ setTimeout(() => setState({
441
+ showMorePrompt: false
442
+ }), 3000);
443
+ }, 1000);
444
+ }
445
+ },
446
+ hasTrack: false,
447
+ options: {
448
+ rewind: true,
449
+ pagination: true,
450
+ arrows: state.arrows,
451
+ classes: {
452
+ pagination: 'splide__pagination splide__pagination-custom'
453
+ },
454
+ trimSpace: true,
455
+ width: state.width,
456
+ perPage: state.perPage,
457
+ gap: state.gap
458
+ },
459
+ children: [/*#__PURE__*/_jsxs("div", {
460
+ className: "splide__arrows",
461
+ children: [/*#__PURE__*/_jsx("button", {
462
+ className: "splide__arrow splide__arrow--prev",
463
+ type: "button",
464
+ disabled: "",
465
+ "aria-label": "Previous slide",
466
+ children: /*#__PURE__*/_jsx(PrevIcon, {
467
+ viewBox: "0 0 40 40"
468
+ })
469
+ }), /*#__PURE__*/_jsx(Tooltip, {
470
+ open: state.showMorePrompt,
471
+ title: t('purchase.morePlanPrompt'),
472
+ placement: "top",
473
+ children: /*#__PURE__*/_jsx("button", {
474
+ className: "splide__arrow splide__arrow--next",
475
+ type: "button",
476
+ "aria-label": "Next slide",
477
+ children: /*#__PURE__*/_jsx(NextIcon, {
478
+ viewBox: "0 0 40 40"
479
+ })
480
+ })
481
+ })]
482
+ }), /*#__PURE__*/_jsx(SplideTrack, {
483
+ children: state.plans.map(x => /*#__PURE__*/_jsx(SplideSlide, {
484
+ style: {
485
+ width: 'auto'
486
+ },
487
+ children: /*#__PURE__*/_jsx(Plan, {
488
+ recommend: !!x.isRecommended,
489
+ checked: x._id === state.planId,
490
+ plan: x,
491
+ paymentMethod: state.paymentMethod,
492
+ paymentMethods: state.paymentMethods,
493
+ onClick: () => handleSelect(x._id)
494
+ })
495
+ }, x._id))
496
+ })]
497
+ })]
498
+ })
499
+ })
500
+ })
501
+ })]
502
+ })
503
+ }), session.user && /*#__PURE__*/_jsx(InProgressSession, {}), state.showRedeem && /*#__PURE__*/_jsx(Connect, {
504
+ open: true,
505
+ popup: true,
506
+ useSocket: false,
507
+ locale: locale,
508
+ action: "redeem",
509
+ prefix: "/did",
510
+ checkFn: create().get,
511
+ onSuccess: handleRedeemed,
512
+ onClose: () => setState({
513
+ showRedeem: false
514
+ }),
515
+ checkTimeout: 60 * 5000,
516
+ showDownload: false,
517
+ extraParams: {
518
+ planId: state.planId || state.plans[0]._id,
519
+ sessionId: state.sessionId
520
+ },
521
+ messages: {
522
+ title: t('redeem.dialog.title'),
523
+ scan: t('redeem.dialog.scan'),
524
+ confirm: t('redeem.dialog.confirm'),
525
+ success: t('redeem.dialog.success')
526
+ }
527
+ })]
528
+ });
529
+ }
530
+ const Container = styled(Layout)`
531
+ .container-inner {
532
+ position: absolute;
533
+ left: 0;
534
+ top: 0;
535
+ width: 100%;
536
+ height: 100%;
537
+ }
538
+
539
+ .plan-list {
540
+ display: flex;
541
+ justify-content: center;
542
+ align-items: center;
543
+
544
+ @media (min-width: 900px) {
545
+ margin-top: 20px;
546
+ }
547
+
548
+ @media (min-width: 680px) and (max-width: 899px) {
549
+ justify-content: center;
550
+ }
551
+
552
+ .toc {
553
+ min-width: 180px;
554
+ margin-right: 20px;
555
+ }
556
+
557
+ button.splide__pagination__page.is-active {
558
+ background: #9397a1;
559
+ }
560
+ }
561
+
562
+ .splide__pagination-custom {
563
+ bottom: -24px;
564
+ }
565
+
566
+ .footer {
567
+ display: flex;
568
+ justify-content: ${props => props.embed ? 'space-between' : 'flex-end'};
569
+ align-item: center;
570
+ margin-top: auto;
571
+ padding-top: 16px;
572
+
573
+ > * {
574
+ margin: 0 8px;
575
+ ${props => props.theme.breakpoints.up('md')} {
576
+ margin: 0 16px;
577
+ }
578
+ }
579
+
580
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
581
+ flex-direction: column;
582
+ align-items: center;
583
+ }
584
+
585
+ .footer-left {
586
+ display: flex;
587
+ gap: 16px;
588
+ align-items: center;
589
+ justify-content: space-between;
590
+
591
+ .select-space {
592
+ order: 1;
593
+ color: #000;
594
+
595
+ &:hover {
596
+ text-decoration: underline !important;
597
+ }
598
+ }
599
+
600
+ .redeem-button {
601
+ order: 1;
602
+ }
603
+
604
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
605
+ width: 100%;
606
+ flex-direction: column;
607
+
608
+ .redeem-button {
609
+ width: 100%;
610
+ order: 2;
611
+ }
612
+ }
613
+ }
614
+
615
+ .button-next {
616
+ min-width: 200px;
617
+
618
+ @media (max-width: ${props => props.theme.breakpoints.values.sm}px) {
619
+ width: 100%;
620
+ margin-top: 20px;
621
+ }
622
+ }
623
+ }
624
+ `;
625
+ PurchasePage.propTypes = {
626
+ disableBack: PropTypes.bool
627
+ };
628
+ PurchasePage.defaultProps = {
629
+ disableBack: false
630
+ };
631
+ export default PurchasePage;