payment-kit 1.15.4 → 1.15.5

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 (30) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/integrations/blocklet/user.ts +30 -0
  3. package/api/src/libs/invoice.ts +41 -0
  4. package/api/src/libs/notification/template/customer-reward-succeeded.ts +41 -20
  5. package/api/src/libs/notification/template/subscription-renew-failed.ts +19 -1
  6. package/api/src/libs/notification/template/subscription-succeeded.ts +94 -9
  7. package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +45 -15
  9. package/api/src/libs/notification/template/subscription-will-renew.ts +28 -11
  10. package/api/src/libs/util.ts +18 -1
  11. package/api/src/locales/en.ts +12 -3
  12. package/api/src/locales/zh.ts +12 -3
  13. package/api/src/queues/payment.ts +3 -1
  14. package/api/src/routes/donations.ts +1 -1
  15. package/api/src/routes/subscriptions.ts +30 -5
  16. package/api/src/routes/usage-records.ts +13 -4
  17. package/api/src/store/migrations/20240910-customer-sync.ts +21 -0
  18. package/api/src/store/models/customer.ts +5 -0
  19. package/blocklet.yml +1 -1
  20. package/package.json +9 -9
  21. package/scripts/sdk.js +25 -2
  22. package/src/components/payment-link/before-pay.tsx +41 -29
  23. package/src/components/pricing-table/product-settings.tsx +37 -25
  24. package/src/pages/admin/index.tsx +0 -1
  25. package/src/pages/admin/payments/intents/detail.tsx +14 -1
  26. package/src/pages/admin/payments/payouts/detail.tsx +6 -1
  27. package/src/pages/admin/products/pricing-tables/create.tsx +3 -0
  28. package/src/pages/checkout/pricing-table.tsx +26 -7
  29. package/src/pages/customer/index.tsx +3 -3
  30. package/src/pages/customer/invoice/past-due.tsx +14 -2
@@ -7,7 +7,7 @@ import prettyMsI18n from 'pretty-ms-i18n';
7
7
  import { getTokenSummaryByDid } from '@api/integrations/arcblock/stake';
8
8
  import { getUserLocale } from '../../../integrations/blocklet/notification';
9
9
  import { translate } from '../../../locales';
10
- import { Customer, Subscription } from '../../../store/models';
10
+ import { Customer, PaymentMethod, Subscription } from '../../../store/models';
11
11
  import { PaymentCurrency } from '../../../store/models/payment-currency';
12
12
  import { PaymentDetail, getPaymentAmountForCycleSubscription } from '../../payment';
13
13
  import { getMainProductName } from '../../product';
@@ -36,6 +36,7 @@ interface SubscriptionTrialWilEndEmailTemplateContext {
36
36
  duration: string;
37
37
 
38
38
  viewSubscriptionLink: string;
39
+ paymentMethod: PaymentMethod | null;
39
40
  }
40
41
 
41
42
  export class SubscriptionTrialWilEndEmailTemplate
@@ -71,6 +72,8 @@ export class SubscriptionTrialWilEndEmailTemplate
71
72
  },
72
73
  })) as PaymentCurrency;
73
74
 
75
+ const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
76
+
74
77
  const userDid = customer.did;
75
78
  const locale = await getUserLocale(userDid);
76
79
  const productName = await getMainProductName(subscription.id);
@@ -116,6 +119,7 @@ export class SubscriptionTrialWilEndEmailTemplate
116
119
  duration,
117
120
 
118
121
  viewSubscriptionLink,
122
+ paymentMethod,
119
123
  };
120
124
  }
121
125
 
@@ -157,7 +161,7 @@ export class SubscriptionTrialWilEndEmailTemplate
157
161
  currentPeriodStart,
158
162
  currentPeriodEnd,
159
163
  duration,
160
-
164
+ paymentMethod,
161
165
  viewSubscriptionLink,
162
166
  } = await this.getContext();
163
167
 
@@ -167,24 +171,27 @@ export class SubscriptionTrialWilEndEmailTemplate
167
171
  return null;
168
172
  }
169
173
 
174
+ const isStripe = paymentMethod?.type === 'stripe';
175
+
170
176
  const template: BaseEmailTemplateType = {
171
177
  title: `${translate('notification.subscriptionTrialWillEnd.title', locale, {
172
178
  productName,
173
179
  willRenewDuration,
174
180
  })}`,
175
- body: canPay
176
- ? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
177
- at,
178
- productName,
179
- willRenewDuration,
180
- })}`
181
- : `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
182
- at,
183
- productName,
184
- willRenewDuration,
185
- balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
186
- price: `${paymentDetail.price} ${paymentDetail.symbol}`,
187
- })}`,
181
+ body:
182
+ canPay || isStripe
183
+ ? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
184
+ at,
185
+ productName,
186
+ willRenewDuration,
187
+ })}`
188
+ : `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
189
+ at,
190
+ productName,
191
+ willRenewDuration,
192
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
193
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
194
+ })}`,
188
195
  // @ts-expect-error
189
196
  attachments: [
190
197
  {
@@ -235,6 +242,29 @@ export class SubscriptionTrialWilEndEmailTemplate
235
242
  text: paymentInfo,
236
243
  },
237
244
  },
245
+ ...(!canPay && !isStripe
246
+ ? [
247
+ {
248
+ type: 'text',
249
+ data: {
250
+ type: 'plain',
251
+ color: '#9397A1',
252
+ text: translate('notification.common.balanceReminder', locale),
253
+ },
254
+ },
255
+ {
256
+ type: 'text',
257
+ data: {
258
+ type: 'plain',
259
+ color: '#FF0000',
260
+ text: translate('notification.subscriptionTrialWillEnd.unableToPayReason', locale, {
261
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
262
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
263
+ }),
264
+ },
265
+ },
266
+ ]
267
+ : []),
238
268
  {
239
269
  type: 'text',
240
270
  data: {
@@ -268,14 +268,6 @@ export class SubscriptionWillRenewEmailTemplate
268
268
  at,
269
269
  productName,
270
270
  willRenewDuration,
271
- reason: `<span style="color: red;">${translate(
272
- 'notification.subscriptionWillRenew.unableToPayReason',
273
- locale,
274
- {
275
- balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
276
- price: `${paymentDetail.price} ${paymentDetail.symbol}`,
277
- }
278
- )}</span>`,
279
271
  })}`,
280
272
  // @ts-expect-error
281
273
  attachments: [
@@ -339,12 +331,37 @@ export class SubscriptionWillRenewEmailTemplate
339
331
  type: 'text',
340
332
  data: {
341
333
  type: 'plain',
342
- ...(!canPay && {
343
- color: 'red',
344
- }),
334
+ ...(!canPay &&
335
+ !isStripe && {
336
+ color: '#FF0000',
337
+ }),
345
338
  text: `${paymentDetail.balance} ${paymentDetail.symbol}`,
346
339
  },
347
340
  },
341
+ ...(!canPay && !isStripe
342
+ ? [
343
+ {
344
+ type: 'text',
345
+ data: {
346
+ type: 'plain',
347
+ color: '#9397A1',
348
+ text: translate('notification.common.balanceReminder', locale),
349
+ },
350
+ },
351
+ {
352
+ type: 'text',
353
+ data: {
354
+ type: 'plain',
355
+ color: '#FF0000',
356
+ text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
357
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
358
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
359
+ }),
360
+ },
361
+ },
362
+ ]
363
+ : []),
364
+
348
365
  {
349
366
  type: 'text',
350
367
  data: {
@@ -6,7 +6,7 @@ import { getWalletDid } from '@blocklet/sdk/lib/did';
6
6
  import { toStakeAddress } from '@arcblock/did-util';
7
7
  import { customAlphabet } from 'nanoid';
8
8
  import type { LiteralUnion } from 'type-fest';
9
- import { withQuery } from 'ufo';
9
+ import { joinURL, withQuery } from 'ufo';
10
10
 
11
11
  import dayjs from './dayjs';
12
12
  import { blocklet, wallet } from './auth';
@@ -272,3 +272,20 @@ export async function getCustomerStakeAddress(customerDid: string, nonce?: strin
272
272
 
273
273
  return toStakeAddress(customerDid, wallet.address, nonce);
274
274
  }
275
+
276
+ export function getCustomerProfileUrl({
277
+ locale = 'en',
278
+ userDid,
279
+ }: {
280
+ locale: LiteralUnion<'en' | 'zh', string>;
281
+ userDid: string;
282
+ }) {
283
+ const { BLOCKLET_APP_URL } = process.env;
284
+ return joinURL(
285
+ BLOCKLET_APP_URL as string,
286
+ withQuery('.well-known/service/user', {
287
+ locale,
288
+ did: userDid,
289
+ })
290
+ );
291
+ }
@@ -38,6 +38,13 @@ export default flat({
38
38
  prepaid: 'Prepaid',
39
39
  postpaid: 'Postpaid',
40
40
  paidType: 'Paid type',
41
+ expandPayment: 'In addition, you have successfully purchased the following products:',
42
+ trialProduct: 'Trial product:',
43
+ subscribeProduct: 'Subscribed product:',
44
+ paymentQuantity: 'Payment quantity',
45
+ qty: '{count} unit',
46
+ failReason: 'Failure reason',
47
+ balanceReminder: 'Balance reminder',
41
48
  },
42
49
 
43
50
  sendTo: 'Sent to',
@@ -55,7 +62,9 @@ export default flat({
55
62
  title: 'The {productName} trial will end soon',
56
63
  body: 'Your trial for {productName} will end in {willRenewDuration}. Please ensure your account balance is sufficient for automatic billing after the trial ends. Thank you for your support and trust!',
57
64
  unableToPayBody:
58
- 'Your trial for {productName} will end in {willRenewDuration}. <span style="color: red;">Your current balance is {balance}, which is less than {price}</span>, Please ensure your balance is sufficient for automatic billing. Thank you for your support and trust!',
65
+ 'Your trial for {productName} will end in {willRenewDuration}.Your current balance is {balance}, which is less than {price}, Please ensure your balance is sufficient for automatic billing. Thank you for your support and trust!',
66
+ unableToPayReason:
67
+ 'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
59
68
  },
60
69
 
61
70
  subscriptionSucceed: {
@@ -77,7 +86,7 @@ export default flat({
77
86
  title: '{productName} automatic payment reminder',
78
87
  body: 'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
79
88
  unableToPayBody:
80
- 'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.\r\n{reason}',
89
+ 'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
81
90
  unableToPayReason:
82
91
  'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
83
92
  renewAmount: 'Payment amount',
@@ -91,7 +100,7 @@ export default flat({
91
100
 
92
101
  subscriptionRenewFailed: {
93
102
  title: '{productName} automatic payment failed',
94
- body: 'We are sorry to inform you that your {productName} failed to go through the automatic payment on {at}. The reason for the failure is {reason}. If you have any questions, please contact us in time. Thank you!',
103
+ body: 'We are sorry to inform you that your {productName} failed to go through the automatic payment on {at}. If you have any questions, please contact us in time. Thank you!',
95
104
  reason: {
96
105
  noDidWallet: 'You have not bound DID Wallet, please bind DID Wallet to ensure sufficient balance',
97
106
  noDelegation: 'Your DID Wallet has not been authorized, please update authorization',
@@ -38,6 +38,13 @@ export default flat({
38
38
  prepaid: '预付费',
39
39
  postpaid: '后付费',
40
40
  paidType: '付费类型',
41
+ expandPayment: '此外,您还成功购买了以下产品:',
42
+ trialProduct: '试用产品:',
43
+ subscribeProduct: '订阅产品:',
44
+ paymentQuantity: '购买数量',
45
+ qty: '{count} 件',
46
+ failReason: '失败原因',
47
+ balanceReminder: '余额提醒',
41
48
  },
42
49
 
43
50
  sendTo: '发送给',
@@ -55,7 +62,9 @@ export default flat({
55
62
  title: '{productName} 试用期即将结束',
56
63
  body: '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。请确保您的账户余额充足,以便在试用期结束后自动完成扣费。感谢您的支持与信任!',
57
64
  unableToPayBody:
58
- '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。<span style="color: red;">您的当前余额为 {balance},不足 {price}</span>,请确保余额充足以便自动完成扣费。感谢您的支持与信任!',
65
+ '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。您的当前余额为 {balance},不足 {price},请确保余额充足以便自动完成扣费。感谢您的支持与信任!',
66
+ unableToPayReason:
67
+ '预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
59
68
  },
60
69
 
61
70
  subscriptionSucceed: {
@@ -77,7 +86,7 @@ export default flat({
77
86
  title: '{productName} 自动扣费提醒',
78
87
  body: '您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费。若有任何疑问或需要帮助,请随时与我们联系。',
79
88
  unableToPayBody:
80
- '您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费,若有任何疑问或者需要帮助,请随时与我们联系。\r\n{reason}',
89
+ '您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费,若有任何疑问或者需要帮助,请随时与我们联系。',
81
90
  unableToPayReason:
82
91
  '预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
83
92
  renewAmount: '扣费金额',
@@ -91,7 +100,7 @@ export default flat({
91
100
 
92
101
  subscriptionRenewFailed: {
93
102
  title: '{productName} 扣费失败',
94
- body: '很抱歉地通知您,您的 {productName} 于 {at} 扣费失败。失败原因为 {reason}。如有任何疑问,请及时联系我们。谢谢!',
103
+ body: '很抱歉地通知您,您的 {productName} 于 {at} 扣费失败。如有任何疑问,请及时联系我们。谢谢!',
95
104
  reason: {
96
105
  noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足',
97
106
  noDelegation: '您的 DID Wallet 尚未授权,请更新授权',
@@ -724,7 +724,9 @@ export const handlePayment = async (job: PaymentJob) => {
724
724
  subscriptionId: subscription.id,
725
725
  invoiceId: invoice.id,
726
726
  });
727
- createEvent('Subscription', 'customer.subscription.renew_failed', subscription);
727
+ if (!subscription.isImmutable()) {
728
+ createEvent('Subscription', 'customer.subscription.renew_failed', subscription);
729
+ }
728
730
  }
729
731
  }
730
732
 
@@ -125,7 +125,7 @@ router.get('/', async (req, res) => {
125
125
  stripUnknown: true,
126
126
  });
127
127
  const { rows, count } = await CheckoutSession.findAndCountAll({
128
- where: { payment_link_id: target, status: 'complete' },
128
+ where: { payment_link_id: target, status: 'complete', livemode: req.livemode },
129
129
  attributes: [
130
130
  'id',
131
131
  'customer_id',
@@ -6,8 +6,7 @@ import isObject from 'lodash/isObject';
6
6
  import pick from 'lodash/pick';
7
7
  import uniq from 'lodash/uniq';
8
8
 
9
- import { literal } from 'sequelize';
10
- import type { Literal } from 'sequelize/types/utils';
9
+ import { literal, OrderItem } from 'sequelize';
11
10
  import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
12
11
  import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
13
12
  import dayjs from '../libs/dayjs';
@@ -76,12 +75,26 @@ const schema = createListParamSchema<{
76
75
  customer_id?: string;
77
76
  customer_did?: string;
78
77
  activeFirst?: boolean;
78
+ order?: string | string[] | OrderItem | OrderItem[];
79
79
  }>({
80
80
  status: Joi.string().empty(''),
81
81
  customer_id: Joi.string().empty(''),
82
82
  customer_did: Joi.string().empty(''),
83
83
  activeFirst: Joi.boolean().optional(),
84
+ order: Joi.alternatives()
85
+ .try(
86
+ Joi.string(),
87
+ Joi.array().items(Joi.string()),
88
+ Joi.array().items(Joi.array().ordered(Joi.string(), Joi.string().valid('ASC', 'DESC').insensitive()))
89
+ )
90
+ .optional(),
84
91
  });
92
+
93
+ const parseOrder = (orderStr: string): OrderItem => {
94
+ const [field, direction] = orderStr.split(':');
95
+ return [field ?? '', (direction?.toUpperCase() as 'ASC' | 'DESC') || 'ASC'];
96
+ };
97
+
85
98
  router.get('/', authMine, async (req, res) => {
86
99
  const { page, pageSize, status, livemode, ...query } = await schema.validateAsync(req.query, {
87
100
  stripUnknown: false,
@@ -118,7 +131,11 @@ router.get('/', authMine, async (req, res) => {
118
131
  where[key] = query[key];
119
132
  });
120
133
 
121
- const order: [Literal | string, 'ASC' | 'DESC'][] = [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']];
134
+ let order: OrderItem[] = [];
135
+ if (query.order) {
136
+ const orderItems = Array.isArray(query.order) ? query.order : [query.order];
137
+ order = orderItems.map((item) => (typeof item === 'string' ? parseOrder(item) : (item as OrderItem)));
138
+ }
122
139
 
123
140
  if (query.activeFirst) {
124
141
  order.unshift([
@@ -127,6 +144,14 @@ router.get('/', authMine, async (req, res) => {
127
144
  ]);
128
145
  }
129
146
 
147
+ const hasCreatedAtOrUpdatedAtOrder = order.some(
148
+ (item) => Array.isArray(item) && ['created_at', 'updated_at'].includes(item[0] as string)
149
+ );
150
+
151
+ if (!hasCreatedAtOrUpdatedAtOrder) {
152
+ order.push(['created_at', query.o === 'asc' ? 'ASC' : 'DESC']);
153
+ }
154
+
130
155
  try {
131
156
  const { rows: list, count } = await Subscription.findAndCountAll({
132
157
  where,
@@ -278,7 +303,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
278
303
  feedback: feedback || 'other',
279
304
  return_stake: canReturnStake && haveStake,
280
305
  slash_stake: slashStake && haveStake,
281
- slash_reason: slashReason,
306
+ slash_reason: slashStake && haveStake ? slashReason : '',
282
307
  },
283
308
  };
284
309
  const now = dayjs().unix() + 3;
@@ -292,7 +317,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
292
317
  comment,
293
318
  return_stake: canReturnStake && haveStake,
294
319
  slash_stake: slashStake && haveStake,
295
- slash_reason: slashReason,
320
+ slash_reason: slashStake && haveStake ? slashReason : '',
296
321
  };
297
322
  updates.canceled_at = now;
298
323
  if (inTrialing) {
@@ -136,10 +136,21 @@ router.get('/summary', auth, async (req, res) => {
136
136
  }
137
137
  });
138
138
 
139
+ const UsageRecordScheme = Joi.object({
140
+ subscription_item_id: Joi.string().required(),
141
+ start: Joi.number().optional(),
142
+ end: Joi.number().optional(),
143
+ livemode: Joi.boolean().empty('').optional(),
144
+ q: Joi.string().empty('').optional(), // query
145
+ o: Joi.string().empty('').optional(), // order
146
+ }).unknown(true);
147
+
139
148
  export function createUsageRecordQueryFn(doc?: Subscription) {
140
149
  return async (req: Request, res: Response) => {
141
- const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
142
-
150
+ const { error, value: query } = await UsageRecordScheme.validate(req.query, { stripUnknown: true });
151
+ if (error) {
152
+ return res.status(400).json({ error: `usage record request query invalid: ${error.message}` });
153
+ }
143
154
  try {
144
155
  const item = await SubscriptionItem.findByPk(query.subscription_item_id);
145
156
  if (!item) {
@@ -159,8 +170,6 @@ export function createUsageRecordQueryFn(doc?: Subscription) {
159
170
  },
160
171
  },
161
172
  order: [['created_at', 'ASC']],
162
- offset: (page - 1) * pageSize,
163
- limit: pageSize,
164
173
  });
165
174
 
166
175
  res.json({ count, list });
@@ -0,0 +1,21 @@
1
+ import { DataTypes } from 'sequelize';
2
+
3
+ import { Migration, safeApplyColumnChanges } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ await safeApplyColumnChanges(context, {
7
+ customers: [
8
+ {
9
+ name: 'last_sync_at',
10
+ field: {
11
+ type: DataTypes.INTEGER,
12
+ allowNull: true,
13
+ },
14
+ },
15
+ ],
16
+ });
17
+ };
18
+
19
+ export const down: Migration = async ({ context }) => {
20
+ await context.removeColumn('customers', 'last_sync_at');
21
+ };
@@ -63,6 +63,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
63
63
 
64
64
  declare created_at: CreationOptional<Date>;
65
65
  declare updated_at: CreationOptional<Date>;
66
+ declare last_sync_at?: number;
66
67
 
67
68
  public static readonly GENESIS_ATTRIBUTES = {
68
69
  id: {
@@ -228,6 +229,10 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
228
229
  type: DataTypes.JSON,
229
230
  defaultValue: {},
230
231
  },
232
+ last_sync_at: {
233
+ type: DataTypes.INTEGER,
234
+ allowNull: true,
235
+ },
231
236
  },
232
237
  {
233
238
  sequelize,
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.15.4
17
+ version: 1.15.5
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.15.4",
3
+ "version": "1.15.5",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -45,18 +45,18 @@
45
45
  "@abtnode/cron": "1.16.30",
46
46
  "@arcblock/did": "^1.18.135",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.10.30",
48
+ "@arcblock/did-connect": "^2.10.33",
49
49
  "@arcblock/did-util": "^1.18.135",
50
50
  "@arcblock/jwt": "^1.18.135",
51
- "@arcblock/ux": "^2.10.30",
51
+ "@arcblock/ux": "^2.10.33",
52
52
  "@arcblock/validator": "^1.18.135",
53
53
  "@blocklet/js-sdk": "1.16.30",
54
54
  "@blocklet/logger": "1.16.30",
55
- "@blocklet/payment-react": "1.15.4",
55
+ "@blocklet/payment-react": "1.15.5",
56
56
  "@blocklet/sdk": "1.16.30",
57
- "@blocklet/ui-react": "^2.10.30",
58
- "@blocklet/uploader": "^0.1.29",
59
- "@blocklet/xss": "^0.1.5",
57
+ "@blocklet/ui-react": "^2.10.33",
58
+ "@blocklet/uploader": "^0.1.35",
59
+ "@blocklet/xss": "^0.1.7",
60
60
  "@mui/icons-material": "^5.16.6",
61
61
  "@mui/lab": "^5.0.0-alpha.173",
62
62
  "@mui/material": "^5.16.6",
@@ -118,7 +118,7 @@
118
118
  "devDependencies": {
119
119
  "@abtnode/types": "1.16.30",
120
120
  "@arcblock/eslint-config-ts": "^0.3.2",
121
- "@blocklet/payment-types": "1.15.4",
121
+ "@blocklet/payment-types": "1.15.5",
122
122
  "@types/cookie-parser": "^1.4.7",
123
123
  "@types/cors": "^2.8.17",
124
124
  "@types/debug": "^4.1.12",
@@ -160,5 +160,5 @@
160
160
  "parser": "typescript"
161
161
  }
162
162
  },
163
- "gitHead": "d1c51f0dffe33e5d42b3dc601a554df6c8f88828"
163
+ "gitHead": "013d9fb8161d6021b66da18916f49242dc156580"
164
164
  }
package/scripts/sdk.js CHANGED
@@ -6,9 +6,19 @@ const payment = require('@blocklet/payment-js').default;
6
6
 
7
7
  (async () => {
8
8
  payment.environments.setTestMode(true);
9
- const paymentIntent = await payment.paymentIntents.retrieve('pi_ybTOCWweEnb9grWZsTH7MCVi');
9
+ const subcriptions = await payment.subscriptions.list({
10
+ order: 'updated_at:ASC',
11
+ // order: [
12
+ // ['status', 'DESC'],
13
+ // ['updated_at', 'ASC'],
14
+ // ], // also support sequelize order
15
+ activeFirst: true,
16
+ });
17
+ console.log('🚀 ~ subcriptions:', subcriptions);
10
18
 
11
- console.log('paymentIntent', paymentIntent);
19
+ // const paymentIntent = await payment.paymentIntents.retrieve('pi_ybTOCWweEnb9grWZsTH7MCVi');
20
+
21
+ // console.log('paymentIntent', paymentIntent);
12
22
 
13
23
  // const refundResult = await payment.paymentIntents.refund('pi_ybTOCWweEnb9grWZsTH7MCVi', {
14
24
  // amount: '0.001',
@@ -81,5 +91,18 @@ const payment = require('@blocklet/payment-js').default;
81
91
  // expires_at: 1721121607,
82
92
  // });
83
93
  // console.log('checkoutSession', checkoutSession);
94
+ // const product = await payment.products.create({
95
+ // name: 'Test SDK product',
96
+ // description: 'test',
97
+ // });
98
+ // console.log('🚀 ~ product:', product);
99
+ // const paymentPrice = await payment.prices.create({
100
+ // product_id: product.id,
101
+ // type: 'one_time',
102
+ // unit_amount: '1',
103
+ // currency_id: 'pc_9l5sh8bcjbLU',
104
+ // });
105
+ // console.log('🚀 ~ paymentPrice:', paymentPrice);
106
+
84
107
  process.exit(0);
85
108
  })();
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
4
- import { useEffect, useState } from 'react';
4
+ import { useEffect, useRef, useState } from 'react';
5
5
  import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
6
6
  import { useSearchParams } from 'react-router-dom';
7
7
 
@@ -29,6 +29,7 @@ export default function BeforePay({
29
29
  const items = useFieldArray({ control, name: 'line_items' });
30
30
  const includeFreeTrial = useWatch({ control, name: 'include_free_trial' });
31
31
  const [state, setState] = useState({ creating: false });
32
+ const containerRef = useRef<HTMLDivElement>(null);
32
33
 
33
34
  useEffect(() => {
34
35
  if (items.fields.length) {
@@ -56,6 +57,14 @@ export default function BeforePay({
56
57
  setValue('invoice_creation.enabled', true);
57
58
  }
58
59
  }
60
+ setTimeout(() => {
61
+ if (containerRef.current) {
62
+ containerRef.current.scrollTo({
63
+ top: containerRef.current.scrollHeight,
64
+ behavior: 'smooth',
65
+ });
66
+ }
67
+ }, 0);
59
68
  }
60
69
  };
61
70
 
@@ -87,35 +96,38 @@ export default function BeforePay({
87
96
  {t('admin.paymentLink.products')} ({getValues().line_items.length})
88
97
  </Typography>
89
98
  <Stack spacing={2} sx={{ width: '100%' }}>
90
- {items.fields.map((item, index) => {
91
- // @ts-ignore
92
- const product = getProductByPriceId(products, item.price_id);
93
- if (!product) {
94
- return null;
95
- }
99
+ <Stack direction="column" sx={{ maxHeight: 500, width: '100%', overflowY: 'auto', gap: 2 }} ref={containerRef}>
100
+ {items.fields.map((item, index) => {
101
+ // @ts-ignore
102
+ const product = getProductByPriceId(products, item.price_id);
103
+ if (!product) {
104
+ return null;
105
+ }
106
+
107
+ return (
108
+ <LineItem
109
+ key={item.id}
110
+ // @ts-ignore
111
+ valid={isPriceAligned(items.fields, products, index).aligned}
112
+ prefix={`line_items.${index}`}
113
+ product={product}
114
+ onRemove={() => items.remove(index)}
115
+ onUpdate={refresh}
116
+ />
117
+ );
118
+ })}
119
+ {items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
120
+ <Typography color="error" fontSize="small">
121
+ {t('admin.paymentLink.recurringNotAligned')}
122
+ </Typography>
123
+ )}
124
+ {items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
125
+ <Typography color="error" fontSize="small">
126
+ {t('admin.paymentLink.currencyNotAligned')}
127
+ </Typography>
128
+ )}
129
+ </Stack>
96
130
 
97
- return (
98
- <LineItem
99
- key={item.id}
100
- // @ts-ignore
101
- valid={isPriceAligned(items.fields, products, index).aligned}
102
- prefix={`line_items.${index}`}
103
- product={product}
104
- onRemove={() => items.remove(index)}
105
- onUpdate={refresh}
106
- />
107
- );
108
- })}
109
- {items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
110
- <Typography color="error" fontSize="small">
111
- {t('admin.paymentLink.recurringNotAligned')}
112
- </Typography>
113
- )}
114
- {items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
115
- <Typography color="error" fontSize="small">
116
- {t('admin.paymentLink.currencyNotAligned')}
117
- </Typography>
118
- )}
119
131
  <ProductSelect
120
132
  mode={items.fields.length ? 'waiting' : 'selecting'}
121
133
  onSelect={onProductSelected}