payment-kit 1.13.21 → 1.13.23

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 (34) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +129 -101
  2. package/api/src/jobs/event.ts +18 -10
  3. package/api/src/jobs/invoice.ts +8 -8
  4. package/api/src/jobs/payment.ts +1 -1
  5. package/api/src/jobs/subscription.ts +1 -1
  6. package/api/src/jobs/webhook.ts +9 -11
  7. package/api/src/routes/integrations/stripe.ts +2 -2
  8. package/api/src/store/models/types.ts +1 -0
  9. package/blocklet.yml +1 -1
  10. package/package.json +3 -3
  11. package/src/components/actions.tsx +4 -10
  12. package/src/components/blockchain/tx.tsx +30 -9
  13. package/src/components/checkout/form/phone.tsx +1 -2
  14. package/src/components/checkout/pay.tsx +2 -2
  15. package/src/components/click-boundary.tsx +7 -0
  16. package/src/components/confirm.tsx +2 -18
  17. package/src/components/customer/actions.tsx +3 -2
  18. package/src/components/customer/edit.tsx +2 -1
  19. package/src/components/invoice/action.tsx +3 -2
  20. package/src/components/metadata/form.tsx +14 -8
  21. package/src/components/payment-intent/actions.tsx +4 -3
  22. package/src/components/payment-intent/list.tsx +2 -2
  23. package/src/components/payment-link/actions.tsx +3 -2
  24. package/src/components/payment-link/item.tsx +18 -15
  25. package/src/components/payment-method/stripe.tsx +7 -4
  26. package/src/components/price/actions.tsx +9 -6
  27. package/src/components/product/actions.tsx +3 -2
  28. package/src/components/subscription/actions/index.tsx +3 -2
  29. package/src/components/subscription/items/actions.tsx +17 -14
  30. package/src/libs/util.ts +15 -0
  31. package/src/locales/en.tsx +4 -0
  32. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
  33. package/src/pages/admin/payments/intents/detail.tsx +1 -6
  34. package/src/pages/admin/settings/payment-methods/create.tsx +3 -0
@@ -9,6 +9,7 @@ import {
9
9
  Customer,
10
10
  Invoice,
11
11
  InvoiceItem,
12
+ PaymentMethod,
12
13
  Subscription,
13
14
  SubscriptionItem,
14
15
  TEventExpanded,
@@ -29,6 +30,109 @@ export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExp
29
30
  });
30
31
  }
31
32
 
33
+ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
34
+ const customer = await Customer.findByPk(subscription.customer_id);
35
+ const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
36
+
37
+ let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
38
+ if (invoice) {
39
+ return invoice;
40
+ }
41
+
42
+ // @ts-ignore
43
+ invoice = await Invoice.create({
44
+ // @ts-ignore
45
+ number: await customer.getInvoiceNumber(),
46
+ ...pick(stripeInvoice, [
47
+ 'amount_due',
48
+ 'amount_paid',
49
+ 'amount_remaining',
50
+ 'amount_shipping',
51
+ 'attempt_count',
52
+ 'attempted',
53
+ 'auto_advance',
54
+ 'billing_reason',
55
+ 'collection_method',
56
+ 'custom_fields',
57
+ 'customer_address',
58
+ 'customer_email',
59
+ 'customer_name',
60
+ 'customer_phone',
61
+ 'description',
62
+ 'discounts',
63
+ 'due_date',
64
+ 'effective_at',
65
+ 'ending_balance',
66
+ 'livemode',
67
+ 'paid_out_of_band',
68
+ 'paid',
69
+ 'period_end',
70
+ 'period_start',
71
+ 'starting_balance',
72
+ 'status_transitions',
73
+ 'status',
74
+ 'subtotal_excluding_tax',
75
+ 'subtotal',
76
+ 'tax',
77
+ 'total_discount_amounts',
78
+ 'total',
79
+ ]),
80
+
81
+ currency_id: subscription.currency_id,
82
+ customer_id: subscription.customer_id,
83
+ default_payment_method_id: subscription.default_payment_method_id as string,
84
+ payment_intent_id: '',
85
+ subscription_id: subscription.id,
86
+ checkout_session_id: checkoutSession?.id,
87
+ statement_descriptor: stripeInvoice.statement_descriptor || '',
88
+
89
+ payment_settings: subscription.payment_settings,
90
+ metadata: {
91
+ stripe_id: stripeInvoice.id,
92
+ },
93
+ });
94
+ await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
95
+ logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
96
+
97
+ await Promise.all(
98
+ stripeInvoice.lines.data.map(async (line: any) => {
99
+ const subscriptionItem = line.subscription_item
100
+ ? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
101
+ : null;
102
+
103
+ // @ts-ignore
104
+ const item = await InvoiceItem.create({
105
+ currency_id: subscription.currency_id,
106
+ customer_id: subscription.customer_id,
107
+ price_id: line.price?.metadata.id as string,
108
+ invoice_id: invoice?.id as string,
109
+ subscription_id: subscription.id,
110
+ subscription_item_id: subscriptionItem?.id,
111
+ ...pick(line, [
112
+ 'livemode',
113
+ 'amount',
114
+ 'quantity',
115
+ 'description',
116
+ 'period',
117
+ 'discountable',
118
+ 'discount_amounts',
119
+ 'discounts',
120
+ 'proration',
121
+ 'proration_details',
122
+ ]),
123
+ metadata: {
124
+ stripe_id: line.id,
125
+ },
126
+ });
127
+
128
+ logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
129
+ return item;
130
+ })
131
+ );
132
+
133
+ return invoice;
134
+ }
135
+
32
136
  export async function handleStripeInvoiceCreated(event: TEventExpanded, client: Stripe) {
33
137
  if (['invoice.created', 'payment_intent.created'].includes(event.type) === false) {
34
138
  logger.warn('abort because event type not expected', { id: event.id, type: event.type });
@@ -74,98 +178,7 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
74
178
  const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
75
179
 
76
180
  // create stripe invoice
77
- let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
78
- if (!invoice) {
79
- invoice = await Invoice.create({
80
- // @ts-ignore
81
- number: await customer.getInvoiceNumber(),
82
- ...pick(stripeInvoice, [
83
- 'amount_due',
84
- 'amount_paid',
85
- 'amount_remaining',
86
- 'amount_shipping',
87
- 'attempt_count',
88
- 'attempted',
89
- 'auto_advance',
90
- 'billing_reason',
91
- 'collection_method',
92
- 'custom_fields',
93
- 'customer_address',
94
- 'customer_email',
95
- 'customer_name',
96
- 'customer_phone',
97
- 'description',
98
- 'discounts',
99
- 'due_date',
100
- 'effective_at',
101
- 'ending_balance',
102
- 'livemode',
103
- 'paid_out_of_band',
104
- 'paid',
105
- 'period_end',
106
- 'period_start',
107
- 'starting_balance',
108
- 'status_transitions',
109
- 'status',
110
- 'subtotal_excluding_tax',
111
- 'subtotal',
112
- 'tax',
113
- 'total_discount_amounts',
114
- 'total',
115
- ]),
116
-
117
- currency_id: subscription.currency_id,
118
- customer_id: subscription.customer_id,
119
- default_payment_method_id: subscription.default_payment_method_id as string,
120
- payment_intent_id: '',
121
- subscription_id: subscription.id,
122
- checkout_session_id: checkoutSession?.id,
123
- statement_descriptor: stripeInvoice.statement_descriptor || '',
124
-
125
- payment_settings: subscription.payment_settings,
126
- metadata: {
127
- stripe_id: stripeInvoice.id,
128
- },
129
- });
130
- await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
131
- logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
132
-
133
- await Promise.all(
134
- stripeInvoice.lines.data.map(async (line) => {
135
- const subscriptionItem = line.subscription_item
136
- ? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
137
- : null;
138
-
139
- // @ts-ignore
140
- const item = await InvoiceItem.create({
141
- currency_id: subscription.currency_id,
142
- customer_id: subscription.customer_id,
143
- price_id: line.price?.metadata.id as string,
144
- invoice_id: invoice?.id as string,
145
- subscription_id: subscription.id,
146
- subscription_item_id: subscriptionItem?.id,
147
- ...pick(line, [
148
- 'livemode',
149
- 'amount',
150
- 'quantity',
151
- 'description',
152
- 'period',
153
- 'discountable',
154
- 'discount_amounts',
155
- 'discounts',
156
- 'proration',
157
- 'proration_details',
158
- ]),
159
- metadata: {
160
- stripe_id: line.id,
161
- },
162
- });
163
-
164
- logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
165
- return item;
166
- })
167
- );
168
- }
181
+ const invoice = await ensureStripeInvoice(stripeInvoice, subscription, client);
169
182
 
170
183
  return { subscription, invoice, customer, checkoutSession };
171
184
  }
@@ -186,16 +199,32 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
186
199
  return;
187
200
  }
188
201
 
189
- const localInvoiceId = event.data.object.metadata?.id;
202
+ let localInvoiceId = event.data.object.metadata?.id;
203
+
204
+ // in case we missed some of the events
205
+ const subscriptionId = event.data.object.subscription_details?.metadata?.id;
206
+ const appPid = event.data.object.subscription_details?.metadata?.appPid;
190
207
  if (!localInvoiceId) {
191
- try {
192
- await waitForStripeInvoiceMirrored(event.data.object.id);
193
- } catch (err) {
194
- logger.error('wait for stripe invoice mirror error', { localInvoiceId, error: err });
195
- }
208
+ if (subscriptionId && appPid && appPid === env.appPid) {
209
+ logger.warn('try mirror invoice from stripe', { invoiceId: event.data.object.id });
210
+ const subscription = await Subscription.findByPk(subscriptionId);
211
+ if (subscription) {
212
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
213
+ if (method && method.type === 'stripe') {
214
+ const tmp = await ensureStripeInvoice(event.data.object, subscription, method.getStripe());
215
+ localInvoiceId = tmp.id;
216
+ }
217
+ }
218
+ } else {
219
+ try {
220
+ await waitForStripeInvoiceMirrored(event.data.object.id);
221
+ } catch (err) {
222
+ logger.error('wait for stripe invoice mirror error', { localInvoiceId, error: err });
223
+ }
196
224
 
197
- logger.warn('local invoice id not found in strip event', { localInvoiceId });
198
- return;
225
+ logger.warn('local invoice id not found in strip event', { localInvoiceId });
226
+ return;
227
+ }
199
228
  }
200
229
 
201
230
  const invoice = await Invoice.findByPk(localInvoiceId);
@@ -232,7 +261,6 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
232
261
  return;
233
262
  }
234
263
 
235
- // TODO: handle upcoming invoices?
236
264
  if (event.type === 'invoice.upcoming') {
237
265
  logger.info('received invoice upcoming event', event);
238
266
  }
@@ -3,6 +3,7 @@ import { Op } from 'sequelize';
3
3
  import logger from '../libs/logger';
4
4
  import createQueue from '../libs/queue';
5
5
  import { Event } from '../store/models/event';
6
+ import { WebhookAttempt } from '../store/models/webhook-attempt';
6
7
  import { WebhookEndpoint } from '../store/models/webhook-endpoint';
7
8
  import { getJobId, webhookQueue } from './webhook';
8
9
 
@@ -11,34 +12,41 @@ type EventJob = {
11
12
  };
12
13
 
13
14
  export const handleEvent = async (job: EventJob) => {
14
- logger.info('handleEvent', job);
15
+ logger.info('handle event', job);
15
16
 
16
17
  const event = await Event.findByPk(job.eventId);
17
18
  if (!event) {
18
- logger.warn(`Event not found: ${job.eventId}`);
19
+ logger.warn('event not found', job);
19
20
  return;
20
21
  }
21
22
 
22
23
  if (!event.pending_webhooks) {
23
- logger.warn(`Event already processed: ${job.eventId}`);
24
+ logger.warn('event already processed', job);
24
25
  return;
25
26
  }
26
27
 
27
28
  const webhooks = await WebhookEndpoint.findAll({ where: { status: 'enabled', livemode: event.livemode } });
28
29
  const eventWebhooks = webhooks.filter((webhook) => webhook.enabled_events.includes(event.type));
29
30
  if (eventWebhooks.length === 0) {
30
- logger.info(`No webhook endpoint for event: ${job.eventId}`);
31
+ logger.info('no webhook endpoint for event', job);
31
32
  await event.update({ pending_webhooks: 0 });
32
33
  return;
33
34
  }
34
35
 
35
36
  await event.update({ pending_webhooks: eventWebhooks.length });
36
- eventWebhooks.forEach((webhook) => {
37
- logger.info(`Schedule webhook ${webhook.id} attempt for event: ${job.eventId}`);
38
- webhookQueue.push({
39
- id: getJobId(event.id, webhook.id),
40
- job: { eventId: event.id, webhookId: webhook.id },
37
+ eventWebhooks.forEach(async (webhook) => {
38
+ const attempted = await WebhookAttempt.findOne({
39
+ where: { event_id: event.id, webhook_endpoint_id: webhook.id },
41
40
  });
41
+
42
+ // we should only push webhook if it's not attempted before
43
+ if (!attempted) {
44
+ logger.info('schedule initial attempt for event', job);
45
+ webhookQueue.push({
46
+ id: getJobId(event.id, webhook.id),
47
+ job: { eventId: event.id, webhookId: webhook.id },
48
+ });
49
+ }
42
50
  });
43
51
  };
44
52
 
@@ -68,5 +76,5 @@ export const startEventQueue = async () => {
68
76
  };
69
77
 
70
78
  eventQueue.on('failed', ({ id, job, error }) => {
71
- logger.error('Event job failed', { id, job, error });
79
+ logger.error('event job failed', { id, job, error });
72
80
  });
@@ -18,31 +18,31 @@ type InvoiceJob = {
18
18
  // handle invoice payment
19
19
  // TODO: send invoice to user with email
20
20
  export const handleInvoice = async (job: InvoiceJob) => {
21
- logger.info('handleInvoice', job);
21
+ logger.info('handle invoice', job);
22
22
 
23
23
  const invoice = await Invoice.findByPk(job.invoiceId);
24
24
  if (!invoice) {
25
- logger.warn(`Invoice not found: ${job.invoiceId}`);
25
+ logger.warn(`invoice not found: ${job.invoiceId}`);
26
26
  return;
27
27
  }
28
28
  if (invoice.status !== 'open') {
29
- logger.warn(`Invoice not open: ${job.invoiceId}`);
29
+ logger.warn(`invoice not open: ${job.invoiceId}`);
30
30
  return;
31
31
  }
32
32
  if (invoice.auto_advance === false) {
33
- logger.warn(`Invoice not configured to auto advance: ${job.invoiceId}`);
33
+ logger.warn(`invoice not configured to auto advance: ${job.invoiceId}`);
34
34
  return;
35
35
  }
36
36
 
37
37
  const supportAutoCharge = await PaymentMethod.supportAutoCharge(invoice.default_payment_method_id);
38
38
  if (supportAutoCharge === false) {
39
- logger.warn(`Invoice does not support auto charge: ${job.invoiceId}`);
39
+ logger.warn(`invoice does not support auto charge: ${job.invoiceId}`);
40
40
  return;
41
41
  }
42
42
 
43
43
  // no payment required
44
44
  if (invoice.total === '0') {
45
- logger.warn(`Invoice does not require payment: ${job.invoiceId}`);
45
+ logger.warn(`invoice does not require payment: ${job.invoiceId}`);
46
46
 
47
47
  await invoice.update({
48
48
  paid: true,
@@ -59,7 +59,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
59
59
  const subscription = await Subscription.findByPk(invoice.subscription_id);
60
60
  if (subscription && subscription.status === 'incomplete') {
61
61
  await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
62
- logger.info('Invoice subscription updated', subscription.id);
62
+ logger.info('invoice subscription updated', subscription.id);
63
63
  }
64
64
  }
65
65
 
@@ -67,7 +67,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
67
67
  const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
68
68
  if (checkoutSession && checkoutSession.status === 'open') {
69
69
  await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
70
- logger.info('Invoice checkout session updated', checkoutSession.id);
70
+ logger.info('invoice checkout session updated', checkoutSession.id);
71
71
  }
72
72
  }
73
73
 
@@ -19,7 +19,7 @@ type PaymentJob = {
19
19
  };
20
20
 
21
21
  export const handlePayment = async (job: PaymentJob) => {
22
- logger.info('handlePayment', job);
22
+ logger.info('handle payment', job);
23
23
 
24
24
  const paymentIntent = await PaymentIntent.findByPk(job.paymentIntentId);
25
25
  if (!paymentIntent) {
@@ -21,7 +21,7 @@ type SubscriptionJob = {
21
21
 
22
22
  // generate invoice for subscription periodically
23
23
  export const handleSubscription = async (job: SubscriptionJob) => {
24
- logger.info('handleSubscription', job);
24
+ logger.info('handle subscription', job);
25
25
 
26
26
  const subscription = await Subscription.findByPk(job.subscriptionId);
27
27
  if (!subscription) {
@@ -20,31 +20,29 @@ export const getJobId = (eventId: string, webhookId: string) => {
20
20
 
21
21
  // https://stripe.com/docs/webhooks
22
22
  export const handleWebhook = async (job: WebhookJob) => {
23
- logger.info('handleWebhook', job);
23
+ logger.info('handle webhook', job);
24
24
 
25
25
  const event = await Event.findByPk(job.eventId);
26
26
  if (!event) {
27
- logger.warn(`Event not found when attempt webhook: ${job.eventId}`);
27
+ logger.warn('event not found when attempt webhook', job);
28
28
  return;
29
29
  }
30
30
 
31
31
  const webhook = await WebhookEndpoint.findByPk(job.webhookId);
32
32
  if (!webhook) {
33
- logger.warn(`Webhook not found on attempt: ${job.webhookId}`);
33
+ logger.warn('webhook not found on attempt', job);
34
34
  return;
35
35
  }
36
36
  if (webhook.status !== 'enabled') {
37
- logger.warn(`Webhook disabled on attempt: ${job.webhookId}`);
37
+ logger.warn('webhook disabled on attempt', job);
38
38
  return;
39
39
  }
40
40
 
41
- const lastAttempt = await WebhookAttempt.findOne({
41
+ const lastRetryCount = await WebhookAttempt.max('retry_count', {
42
42
  where: { event_id: event.id, webhook_endpoint_id: webhook.id },
43
- order: [['retry_count', 'DESC']],
44
- attributes: ['retry_count'],
45
43
  });
46
44
 
47
- const retryCount = lastAttempt ? lastAttempt.retry_count + 1 : 0;
45
+ const retryCount = lastRetryCount ? +lastRetryCount + 1 : 1;
48
46
 
49
47
  try {
50
48
  // verify similar to component call, but supports external urls
@@ -72,9 +70,9 @@ export const handleWebhook = async (job: WebhookJob) => {
72
70
  });
73
71
 
74
72
  await event.decrement('pending_webhooks');
75
- logger.info(`Webhook attempt success: ${job.webhookId}`);
73
+ logger.info('webhook attempt success', { ...job, retryCount });
76
74
  } catch (err: any) {
77
- logger.error(`Webhook attempt error: ${job.webhookId}`, { message: err.message });
75
+ logger.warn('webhook attempt error', { ...job, retryCount, message: err.message });
78
76
  await WebhookAttempt.create({
79
77
  livemode: event.livemode,
80
78
  event_id: event.id,
@@ -91,7 +89,7 @@ export const handleWebhook = async (job: WebhookJob) => {
91
89
  webhookQueue.push({
92
90
  id: getJobId(event.id, webhook.id),
93
91
  job: { eventId: event.id, webhookId: webhook.id },
94
- runAt: getNextRetry(retryCount + 1),
92
+ runAt: getNextRetry(retryCount),
95
93
  });
96
94
  });
97
95
  }
@@ -43,14 +43,14 @@ const handleEvent = async (req: Request, res: Response) => {
43
43
 
44
44
  if (STRIPE_EVENTS.includes(stripeEvent.type) === false) {
45
45
  logger.debug('webhook event not interested', { id: stripeEvent.id, type: stripeEvent.type });
46
- return res.status(400).json({ error: 'Not implemented' });
46
+ return res.json({ skipped: true });
47
47
  }
48
48
 
49
49
  // only events from this app should be processed
50
50
  const appPid = get(stripeEvent, 'data.object.metadata.appPid');
51
51
  if (appPid && appPid !== env.appPid) {
52
52
  logger.debug('webhook event for other app', { id: stripeEvent.id, type: stripeEvent.type });
53
- return res.json({ received: true });
53
+ return res.json({ skipped: true });
54
54
  }
55
55
 
56
56
  logger.debug('webhook received event', { id: stripeEvent.id, type: stripeEvent.type });
@@ -210,6 +210,7 @@ export type PaymentMethodOptions = {
210
210
 
211
211
  export type PaymentMethodSettings = {
212
212
  stripe?: {
213
+ dashboard: string;
213
214
  publishable_key: string;
214
215
  secret_key: string;
215
216
  webhook_signing_secret: string;
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.21
17
+ version: 1.13.23
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.21",
3
+ "version": "1.13.23",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -100,7 +100,7 @@
100
100
  "devDependencies": {
101
101
  "@arcblock/eslint-config": "^0.2.4",
102
102
  "@arcblock/eslint-config-ts": "^0.2.4",
103
- "@did-pay/types": "1.13.21",
103
+ "@did-pay/types": "1.13.23",
104
104
  "@types/cookie-parser": "^1.4.4",
105
105
  "@types/cors": "^2.8.14",
106
106
  "@types/dotenv-flow": "^3.3.1",
@@ -137,5 +137,5 @@
137
137
  "parser": "typescript"
138
138
  }
139
139
  },
140
- "gitHead": "9a767485dd2e712fdaf1e636d73556c90f20e2bd"
140
+ "gitHead": "a4ba80b4e2d020c912361af6fd555f8bfc52f54f"
141
141
  }
@@ -4,6 +4,8 @@ import { Button, IconButton, ListItemText, Menu, MenuItem } from '@mui/material'
4
4
  import React, { useState } from 'react';
5
5
  import type { LiteralUnion } from 'type-fest';
6
6
 
7
+ import { stopEvent } from '../libs/util';
8
+
7
9
  type ActionItem = {
8
10
  label: string;
9
11
  handler: Function;
@@ -30,20 +32,12 @@ export default function Actions(props: ActionsProps) {
30
32
  const open = Boolean(anchorEl);
31
33
 
32
34
  const onOpen = (e: React.SyntheticEvent<any>) => {
33
- try {
34
- e.stopPropagation();
35
- e.preventDefault();
36
- // eslint-disable-next-line no-empty
37
- } catch {}
35
+ stopEvent(e);
38
36
  setAnchorEl(e.currentTarget);
39
37
  };
40
38
 
41
39
  const onClose = (e: React.SyntheticEvent<any>, handler?: Function) => {
42
- try {
43
- e.stopPropagation();
44
- e.preventDefault();
45
- // eslint-disable-next-line no-empty
46
- } catch {}
40
+ stopEvent(e);
47
41
  setAnchorEl(null);
48
42
 
49
43
  if (typeof handler === 'function') {
@@ -1,27 +1,48 @@
1
- import type { TPaymentMethod } from '@did-pay/types';
1
+ import type { PaymentDetails, TPaymentMethod } from '@did-pay/types';
2
2
  import { OpenInNewOutlined } from '@mui/icons-material';
3
3
  import { Link, Stack, Typography } from '@mui/material';
4
4
  import { joinURL } from 'ufo';
5
5
 
6
- const getTxLink = (method: TPaymentMethod, hash: string) => {
6
+ const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
7
7
  if (method.type === 'arcblock') {
8
- return joinURL(method.settings.arcblock?.explorer_host as string, '/tx', hash);
8
+ return {
9
+ link: joinURL(method.settings.arcblock?.explorer_host as string, '/tx', details.arcblock?.tx_hash as string),
10
+ text: details.arcblock?.tx_hash as string,
11
+ };
9
12
  }
10
13
  if (method.type === 'bitcoin') {
11
- return joinURL(method.settings.bitcoin?.explorer_host as string, '/tx', hash);
14
+ return {
15
+ link: joinURL(method.settings.bitcoin?.explorer_host as string, '/tx', details.bitcoin?.tx_hash as string),
16
+ text: details.bitcoin?.tx_hash as string,
17
+ };
12
18
  }
13
19
  if (method.type === 'ethereum') {
14
- return joinURL(method.settings.ethereum?.explorer_host as string, '/tx', hash);
20
+ return {
21
+ link: joinURL(method.settings.ethereum?.explorer_host as string, '/tx', details.ethereum?.tx_hash as string),
22
+ text: details.ethereum?.tx_hash as string,
23
+ };
24
+ }
25
+ if (method.type === 'stripe') {
26
+ const dashboard = method.livemode ? 'https://dashboard.stripe.com' : 'https://dashboard.stripe.com/test';
27
+ return {
28
+ link: joinURL(
29
+ method.settings.stripe?.dashboard || dashboard,
30
+ 'payments',
31
+ details.stripe?.payment_intent_id as string
32
+ ),
33
+ text: details.stripe?.payment_intent_id as string,
34
+ };
15
35
  }
16
36
 
17
- return hash;
37
+ return { text: 'N/A', link: '' };
18
38
  };
19
39
 
20
- export default function TxLink(props: { hash: string; method: TPaymentMethod }) {
40
+ export default function TxLink(props: { details: PaymentDetails; method: TPaymentMethod }) {
41
+ const { text, link } = getTxLink(props.method, props.details);
21
42
  return (
22
- <Link href={getTxLink(props.method, props.hash)} target="_blank" rel="noopener noreferrer">
43
+ <Link href={link} target="_blank" rel="noopener noreferrer">
23
44
  <Stack component="span" direction="row" alignItems="center" spacing={1}>
24
- <Typography component="span">{props.hash}</Typography>
45
+ <Typography component="span">{text}</Typography>
25
46
  <OpenInNewOutlined fontSize="small" />
26
47
  </Stack>
27
48
  </Link>
@@ -5,10 +5,9 @@ import { useEffect } from 'react';
5
5
  import { useFormContext, useWatch } from 'react-hook-form';
6
6
  import { CountryIso2, FlagEmoji, defaultCountries, parseCountry, usePhoneInput } from 'react-international-phone';
7
7
 
8
+ import { isValidCountry } from '../../../libs/util';
8
9
  import FormInput from '../../input';
9
10
 
10
- const isValidCountry = (code: string) => defaultCountries.some((x) => x[1] === code);
11
-
12
11
  export default function PhoneInput({ ...props }) {
13
12
  const countryFieldName = props.countryFieldName || 'billing_address.country';
14
13
 
@@ -14,7 +14,7 @@ import { FormProvider, useForm } from 'react-hook-form';
14
14
 
15
15
  import { useSessionContext } from '../../contexts/session';
16
16
  import { useSettingsContext } from '../../contexts/settings';
17
- import { findCurrency, formatError, getStatementDescriptor } from '../../libs/util';
17
+ import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../../libs/util';
18
18
  import PaymentError from './error';
19
19
  import CheckoutFooter from './footer';
20
20
  import PaymentForm from './form';
@@ -175,7 +175,7 @@ export function CheckoutPayMain({
175
175
  postal_code: '',
176
176
  },
177
177
  customer?.address || {},
178
- { country: customer?.address?.country || 'us' }
178
+ { country: isValidCountry(customer?.address?.country || '') ? customer?.address?.country : 'us' }
179
179
  ),
180
180
  },
181
181
  });
@@ -0,0 +1,7 @@
1
+ import { Box } from '@mui/material';
2
+
3
+ import { stopEvent } from '../libs/util';
4
+
5
+ export default function ClickBoundary({ children }: { children: React.ReactNode }) {
6
+ return <Box onClick={stopEvent}>{children}</Box>;
7
+ }
@@ -17,29 +17,13 @@ export default function ConfirmDialog({
17
17
  loading?: boolean;
18
18
  }) {
19
19
  const { t } = useLocaleContext();
20
- const handleConfirm = (e: any) => {
21
- try {
22
- e.stopPropagation();
23
- e.preventDefault();
24
- // eslint-disable-next-line no-empty
25
- } catch {}
26
- onConfirm(e);
27
- };
28
- const handleCancel = (e: any) => {
29
- try {
30
- e.stopPropagation();
31
- e.preventDefault();
32
- // eslint-disable-next-line no-empty
33
- } catch {}
34
- onCancel(e);
35
- };
36
20
 
37
21
  return (
38
22
  <Confirm
39
23
  open
40
24
  title={title}
41
- onConfirm={handleConfirm}
42
- onCancel={handleCancel}
25
+ onConfirm={onConfirm}
26
+ onCancel={onCancel}
43
27
  confirmButton={{
44
28
  text: t('common.confirm'),
45
29
  props: { color: 'error', size: 'small', variant: 'contained', disabled: !!loading },
@@ -7,6 +7,7 @@ import type { LiteralUnion } from 'type-fest';
7
7
  import api from '../../libs/api';
8
8
  import { formatError } from '../../libs/util';
9
9
  import Actions from '../actions';
10
+ import ClickBoundary from '../click-boundary';
10
11
  import ConfirmDialog from '../confirm';
11
12
 
12
13
  type Props = {
@@ -41,7 +42,7 @@ export default function CustomerActions({ data, onChange, variant }: Props) {
41
42
  };
42
43
 
43
44
  return (
44
- <>
45
+ <ClickBoundary>
45
46
  <Actions
46
47
  variant={variant}
47
48
  actions={[
@@ -68,6 +69,6 @@ export default function CustomerActions({ data, onChange, variant }: Props) {
68
69
  loading={state.loading}
69
70
  />
70
71
  )}
71
- </>
72
+ </ClickBoundary>
72
73
  );
73
74
  }
@@ -5,6 +5,7 @@ import { Button, CircularProgress, Stack } from '@mui/material';
5
5
  import type { EventHandler } from 'react';
6
6
  import { FormProvider, useForm } from 'react-hook-form';
7
7
 
8
+ import { isValidCountry } from '../../libs/util';
8
9
  import CustomerForm from './form';
9
10
 
10
11
  export default function EditCustomer({
@@ -34,7 +35,7 @@ export default function EditCustomer({
34
35
  postal_code: '',
35
36
  },
36
37
  data.address || {},
37
- { country: data.address?.country || 'us' }
38
+ { country: isValidCountry(data.address?.country || '') ? data.address?.country : 'us' }
38
39
  ),
39
40
  },
40
41
  });
@@ -8,6 +8,7 @@ import type { LiteralUnion } from 'type-fest';
8
8
  import api from '../../libs/api';
9
9
  import { formatError } from '../../libs/util';
10
10
  import Actions from '../actions';
11
+ import ClickBoundary from '../click-boundary';
11
12
  import ConfirmDialog from '../confirm';
12
13
 
13
14
  type Props = {
@@ -78,7 +79,7 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
78
79
  }
79
80
 
80
81
  return (
81
- <>
82
+ <ClickBoundary>
82
83
  <Actions variant={variant} actions={actions} />
83
84
  {state.action === 'xxx' && (
84
85
  <ConfirmDialog
@@ -89,6 +90,6 @@ export default function InvoiceActions({ data, variant, onChange }: Props) {
89
90
  loading={state.loading}
90
91
  />
91
92
  )}
92
- </>
93
+ </ClickBoundary>
93
94
  );
94
95
  }
@@ -1,7 +1,9 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
  import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
3
- import { Box, Button, IconButton, Stack, TextField, Typography } from '@mui/material';
4
- import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
3
+ import { Box, Button, IconButton, Stack, Typography } from '@mui/material';
4
+ import { useFieldArray, useFormContext } from 'react-hook-form';
5
+
6
+ import FormInput from '../input';
5
7
 
6
8
  export default function MetadataForm({ title, actions }: { title?: string; actions?: React.ReactNode }) {
7
9
  const { t } = useLocaleContext();
@@ -13,15 +15,19 @@ export default function MetadataForm({ title, actions }: { title?: string; actio
13
15
  {metadata.fields.map((meta, index) => (
14
16
  <Stack key={meta.id} mt={2} spacing={2} direction="row" alignItems="center">
15
17
  <Stack direction="row" spacing={2}>
16
- <Controller
17
- render={({ field }) => <TextField {...field} sx={{ flex: 1 }} placeholder="Key" size="small" />}
18
+ <FormInput
19
+ sx={{ flex: 1 }}
20
+ size="small"
18
21
  name={`metadata.${index}.key`}
19
- control={control}
22
+ rules={{ required: t('checkout.required') }}
23
+ placeholder="Key"
20
24
  />
21
- <Controller
22
- render={({ field }) => <TextField {...field} sx={{ flex: 2 }} placeholder="Value" size="small" />}
25
+ <FormInput
26
+ sx={{ flex: 2 }}
27
+ size="small"
23
28
  name={`metadata.${index}.value`}
24
- control={control}
29
+ placeholder="Value"
30
+ rules={{ required: t('checkout.required') }}
25
31
  />
26
32
  </Stack>
27
33
  <IconButton size="small" onClick={() => metadata.remove(index)}>
@@ -8,6 +8,7 @@ import type { LiteralUnion } from 'type-fest';
8
8
  import api from '../../libs/api';
9
9
  import { formatError } from '../../libs/util';
10
10
  import Actions from '../actions';
11
+ import ClickBoundary from '../click-boundary';
11
12
  import ConfirmDialog from '../confirm';
12
13
 
13
14
  type Props = {
@@ -58,14 +59,14 @@ export default function PaymentIntentActions({ data, variant }: Props) {
58
59
  if (variant === 'compact') {
59
60
  actions.push({
60
61
  label: t('admin.paymentIntent.view'),
61
- handler: () => navigate(`/admin/payments/${data.customer_id}`),
62
+ handler: () => navigate(`/admin/payments/${data.id}`),
62
63
  color: 'primary',
63
64
  disabled: false,
64
65
  });
65
66
  }
66
67
 
67
68
  return (
68
- <>
69
+ <ClickBoundary>
69
70
  <Actions variant={variant} actions={actions} />
70
71
  {state.action === 'refund' && (
71
72
  <ConfirmDialog
@@ -76,6 +77,6 @@ export default function PaymentIntentActions({ data, variant }: Props) {
76
77
  loading={state.loading}
77
78
  />
78
79
  )}
79
- </>
80
+ </ClickBoundary>
80
81
  );
81
82
  }
@@ -6,7 +6,7 @@ import { Alert, CircularProgress, ToggleButton, ToggleButtonGroup, Typography }
6
6
  import { fromUnitToToken } from '@ocap/util';
7
7
  import { useRequest } from 'ahooks';
8
8
  import { useEffect, useState } from 'react';
9
- import { Link, useNavigate } from 'react-router-dom';
9
+ import { useNavigate } from 'react-router-dom';
10
10
 
11
11
  import api from '../../libs/api';
12
12
  import { formatTime, getPaymentIntentStatusColor } from '../../libs/util';
@@ -152,7 +152,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
152
152
  options: {
153
153
  customBodyRenderLite: (_: string, index: number) => {
154
154
  const item = data.list[index] as TPaymentIntentExpanded;
155
- return <Link to={`/admin/customers/${item?.customer.id}`}>{item?.customer.email}</Link>;
155
+ return item.customer.email;
156
156
  },
157
157
  },
158
158
  });
@@ -7,6 +7,7 @@ import type { LiteralUnion } from 'type-fest';
7
7
  import api from '../../libs/api';
8
8
  import { formatError } from '../../libs/util';
9
9
  import Actions from '../actions';
10
+ import ClickBoundary from '../click-boundary';
10
11
  import ConfirmDialog from '../confirm';
11
12
  import RenamePaymentLink from './rename';
12
13
 
@@ -68,7 +69,7 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
68
69
  };
69
70
 
70
71
  return (
71
- <>
72
+ <ClickBoundary>
72
73
  <Actions
73
74
  variant={variant}
74
75
  actions={[
@@ -109,6 +110,6 @@ export default function PaymentLinkActions({ data, variant, onChange }: Props) {
109
110
  loading={state.loading}
110
111
  />
111
112
  )}
112
- </>
113
+ </ClickBoundary>
113
114
  );
114
115
  }
@@ -9,6 +9,7 @@ import { useSettingsContext } from '../../contexts/settings';
9
9
  import api from '../../libs/api';
10
10
  import { formatError, formatPrice } from '../../libs/util';
11
11
  import Actions from '../actions';
12
+ import ClickBoundary from '../click-boundary';
12
13
  import InfoCard from '../info-card';
13
14
  import EditProduct from '../product/edit';
14
15
 
@@ -52,21 +53,23 @@ export default function LineItem({ prefix, product, valid, onUpdate, onRemove }:
52
53
  borderRadius: 2,
53
54
  position: 'relative',
54
55
  }}>
55
- <Actions
56
- sx={{ position: 'absolute', top: 0, right: 0 }}
57
- actions={[
58
- {
59
- label: t('admin.product.edit'),
60
- handler: () => setState({ editing: true }),
61
- color: 'primary',
62
- },
63
- {
64
- label: t('admin.product.remove'),
65
- handler: onRemove,
66
- color: 'error',
67
- },
68
- ]}
69
- />
56
+ <ClickBoundary>
57
+ <Actions
58
+ sx={{ position: 'absolute', top: 0, right: 0 }}
59
+ actions={[
60
+ {
61
+ label: t('admin.product.edit'),
62
+ handler: () => setState({ editing: true }),
63
+ color: 'primary',
64
+ },
65
+ {
66
+ label: t('admin.product.remove'),
67
+ handler: onRemove,
68
+ color: 'error',
69
+ },
70
+ ]}
71
+ />
72
+ </ClickBoundary>
70
73
  <Stack direction="column" alignItems="flex-start">
71
74
  <InfoCard
72
75
  logo={product.images[0]}
@@ -9,7 +9,6 @@ export default function StripeMethodForm() {
9
9
  return (
10
10
  <>
11
11
  <FormInput
12
- key="name"
13
12
  name="name"
14
13
  type="text"
15
14
  rules={{ required: true }}
@@ -17,7 +16,6 @@ export default function StripeMethodForm() {
17
16
  placeholder={t('admin.paymentMethod.name.tip')}
18
17
  />
19
18
  <FormInput
20
- key="description"
21
19
  name="description"
22
20
  type="text"
23
21
  rules={{ required: true }}
@@ -25,7 +23,13 @@ export default function StripeMethodForm() {
25
23
  placeholder={t('admin.paymentMethod.description.tip')}
26
24
  />
27
25
  <FormInput
28
- key="publishable_key"
26
+ name="settings.stripe.dashboard"
27
+ type="text"
28
+ rules={{ required: true }}
29
+ label={t('admin.paymentMethod.stripe.dashboard.label')}
30
+ placeholder={t('admin.paymentMethod.stripe.dashboard.tip')}
31
+ />
32
+ <FormInput
29
33
  name="settings.stripe.publishable_key"
30
34
  type="text"
31
35
  rules={{ required: true }}
@@ -33,7 +37,6 @@ export default function StripeMethodForm() {
33
37
  placeholder={t('admin.paymentMethod.stripe.publishable_key.tip')}
34
38
  />
35
39
  <FormInput
36
- key="secret_key"
37
40
  name="settings.stripe.secret_key"
38
41
  type="password"
39
42
  rules={{ required: true }}
@@ -1,6 +1,7 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
2
 
3
3
  import Actions from '../actions';
4
+ import ClickBoundary from '../click-boundary';
4
5
 
5
6
  type PriceActionProps = {
6
7
  onDuplicate: Function;
@@ -11,11 +12,13 @@ export default function PriceActions(props: PriceActionProps) {
11
12
  const { t } = useLocaleContext();
12
13
 
13
14
  return (
14
- <Actions
15
- actions={[
16
- { label: t('admin.price.duplicate'), handler: props.onDuplicate, color: 'primary' },
17
- { label: t('admin.price.remove'), handler: props.onRemove, color: 'error' },
18
- ]}
19
- />
15
+ <ClickBoundary>
16
+ <Actions
17
+ actions={[
18
+ { label: t('admin.price.duplicate'), handler: props.onDuplicate, color: 'primary' },
19
+ { label: t('admin.price.remove'), handler: props.onRemove, color: 'error' },
20
+ ]}
21
+ />
22
+ </ClickBoundary>
20
23
  );
21
24
  }
@@ -7,6 +7,7 @@ import type { LiteralUnion } from 'type-fest';
7
7
  import api from '../../libs/api';
8
8
  import { formatError } from '../../libs/util';
9
9
  import Actions from '../actions';
10
+ import ClickBoundary from '../click-boundary';
10
11
  import ConfirmDialog from '../confirm';
11
12
  import EditProduct from './edit';
12
13
 
@@ -68,7 +69,7 @@ export default function ProductActions({ data, variant, onChange }: ProductActio
68
69
  };
69
70
 
70
71
  return (
71
- <>
72
+ <ClickBoundary>
72
73
  <Actions
73
74
  variant={variant}
74
75
  actions={[
@@ -120,6 +121,6 @@ export default function ProductActions({ data, variant, onChange }: ProductActio
120
121
  loading={state.loading}
121
122
  />
122
123
  )}
123
- </>
124
+ </ClickBoundary>
124
125
  );
125
126
  }
@@ -9,6 +9,7 @@ import type { LiteralUnion } from 'type-fest';
9
9
  import api from '../../../libs/api';
10
10
  import { formatError } from '../../../libs/util';
11
11
  import Actions from '../../actions';
12
+ import ClickBoundary from '../../click-boundary';
12
13
  import ConfirmDialog from '../../confirm';
13
14
  import SubscriptionCancelForm from './cancel';
14
15
  import SubscriptionPauseForm from './pause';
@@ -111,7 +112,7 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
111
112
  }
112
113
 
113
114
  return (
114
- <>
115
+ <ClickBoundary>
115
116
  <Actions variant={variant} actions={actions} />
116
117
  {state.action === 'cancel' && (
117
118
  <ConfirmDialog
@@ -140,7 +141,7 @@ function SubscriptionActionsInner({ data, variant, onChange }: Props) {
140
141
  loading={state.loading}
141
142
  />
142
143
  )}
143
- </>
144
+ </ClickBoundary>
144
145
  );
145
146
  }
146
147
 
@@ -3,6 +3,7 @@ import type { TLineItemExpanded } from '@did-pay/types';
3
3
  import { useNavigate } from 'react-router-dom';
4
4
 
5
5
  import Actions from '../../actions';
6
+ import ClickBoundary from '../../click-boundary';
6
7
 
7
8
  type Props = {
8
9
  data: TLineItemExpanded;
@@ -13,19 +14,21 @@ export default function LineItemActions(props: Props) {
13
14
  const navigate = useNavigate();
14
15
 
15
16
  return (
16
- <Actions
17
- actions={[
18
- {
19
- label: t('admin.price.view'),
20
- handler: () => navigate(`/admin/products/${props.data.price_id}`),
21
- color: 'primary',
22
- },
23
- {
24
- label: t('admin.product.view'),
25
- handler: () => navigate(`/admin/products/${props.data.price.product_id}`),
26
- color: 'primary',
27
- },
28
- ]}
29
- />
17
+ <ClickBoundary>
18
+ <Actions
19
+ actions={[
20
+ {
21
+ label: t('admin.price.view'),
22
+ handler: () => navigate(`/admin/products/${props.data.price_id}`),
23
+ color: 'primary',
24
+ },
25
+ {
26
+ label: t('admin.product.view'),
27
+ handler: () => navigate(`/admin/products/${props.data.price.product_id}`),
28
+ color: 'primary',
29
+ },
30
+ ]}
31
+ />
32
+ </ClickBoundary>
30
33
  );
31
34
  }
package/src/libs/util.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  import { BN, fromUnitToToken } from '@ocap/util';
16
16
  import cloneDeep from 'lodash/cloneDeep';
17
17
  import isEqual from 'lodash/isEqual';
18
+ import { defaultCountries } from 'react-international-phone';
18
19
 
19
20
  import dayjs from './dayjs';
20
21
 
@@ -567,3 +568,17 @@ export function getSupportedPaymentCurrencies(items: TLineItemExpanded[]) {
567
568
  }, []);
568
569
  return Array.from(new Set(currencies));
569
570
  }
571
+
572
+ export function isValidCountry(code: string) {
573
+ return defaultCountries.some((x) => x[1] === code);
574
+ }
575
+
576
+ export function stopEvent(e: React.SyntheticEvent<any>) {
577
+ try {
578
+ e.stopPropagation();
579
+ e.preventDefault();
580
+ // eslint-disable-next-line no-empty
581
+ } catch {
582
+ // Do nothing
583
+ }
584
+ }
@@ -232,6 +232,10 @@ export default flat({
232
232
  tip: 'Not consumer facing',
233
233
  },
234
234
  stripe: {
235
+ dashboard: {
236
+ label: 'Dashboard URL',
237
+ tip: 'Used to generate links to Stripe dashboard',
238
+ },
235
239
  publishable_key: {
236
240
  label: 'Publishable Key',
237
241
  tip: 'Publishable Key, See Dashboard > Developers > API Keys',
@@ -176,7 +176,7 @@ export default function SubscriptionDetail(props: { id: string }) {
176
176
  {data.payment_details?.arcblock?.tx_hash && (
177
177
  <InfoRow
178
178
  label={t('common.txHash')}
179
- value={<TxLink hash={data.payment_details.arcblock?.tx_hash} method={data.paymentMethod} />}
179
+ value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
180
180
  />
181
181
  )}
182
182
  </Stack>
@@ -141,12 +141,7 @@ export default function PaymentIntentDetail(props: { id: string }) {
141
141
  />
142
142
  <InfoRow
143
143
  label={t('common.txHash')}
144
- value={
145
- <TxLink
146
- hash={data.payment_details?.arcblock?.tx_hash || data.metadata?.txHash}
147
- method={data.paymentMethod}
148
- />
149
- }
144
+ value={<TxLink details={data.payment_details as any} method={data.paymentMethod} />}
150
145
  />
151
146
  </Stack>
152
147
  </Box>
@@ -9,11 +9,13 @@ import { dispatch } from 'use-bus';
9
9
 
10
10
  import DrawerForm from '../../../../components/drawer-form';
11
11
  import PaymentMethodForm from '../../../../components/payment-method/form';
12
+ import { useSettingsContext } from '../../../../contexts/settings';
12
13
  import api from '../../../../libs/api';
13
14
  import { formatError } from '../../../../libs/util';
14
15
 
15
16
  export default function PaymentMethodCreate() {
16
17
  const { t } = useLocaleContext();
18
+ const settings = useSettingsContext();
17
19
 
18
20
  const methods = useForm<TPaymentMethod>({
19
21
  defaultValues: {
@@ -27,6 +29,7 @@ export default function PaymentMethodCreate() {
27
29
  explorer_host: '',
28
30
  },
29
31
  stripe: {
32
+ dashboard: settings.livemode ? 'https://dashboard.stripe.com' : 'https://dashboard.stripe.com/test',
30
33
  publishable_key: '',
31
34
  secret_key: '',
32
35
  },