payment-kit 1.13.159 → 1.13.161

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 (39) hide show
  1. package/api/src/hooks/pre-flight.ts +0 -3
  2. package/api/src/index.ts +4 -2
  3. package/api/src/integrations/stripe/handlers/invoice.ts +6 -0
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +53 -6
  5. package/api/src/integrations/stripe/handlers/subscription.ts +5 -3
  6. package/api/src/integrations/stripe/resource.ts +12 -4
  7. package/api/src/libs/subscription.ts +12 -1
  8. package/api/src/routes/checkout-sessions.ts +0 -1
  9. package/api/src/routes/connect/change-payment.ts +106 -0
  10. package/api/src/routes/connect/{update.ts → change-plan.ts} +1 -1
  11. package/api/src/routes/connect/setup.ts +5 -3
  12. package/api/src/routes/connect/shared.ts +50 -11
  13. package/api/src/routes/invoices.ts +24 -0
  14. package/api/src/routes/payment-intents.ts +24 -0
  15. package/api/src/routes/refunds.ts +24 -0
  16. package/api/src/routes/subscriptions.ts +254 -6
  17. package/api/src/store/migrate.ts +1 -1
  18. package/api/src/store/models/setup-intent.ts +2 -5
  19. package/blocklet.yml +1 -1
  20. package/package.json +14 -14
  21. package/src/app.tsx +14 -4
  22. package/src/components/metadata/list.tsx +25 -0
  23. package/src/components/price/currency-select.tsx +1 -1
  24. package/src/components/subscription/portal/actions.tsx +4 -6
  25. package/src/libs/util.ts +7 -21
  26. package/src/pages/admin/billing/invoices/detail.tsx +2 -10
  27. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -9
  28. package/src/pages/admin/customers/customers/detail.tsx +3 -3
  29. package/src/pages/admin/payments/intents/detail.tsx +2 -10
  30. package/src/pages/admin/payments/links/detail.tsx +6 -14
  31. package/src/pages/admin/payments/refunds/detail.tsx +2 -10
  32. package/src/pages/admin/products/prices/detail.tsx +6 -13
  33. package/src/pages/admin/products/pricing-tables/detail.tsx +6 -14
  34. package/src/pages/admin/products/products/detail.tsx +6 -14
  35. package/src/pages/customer/invoice/past-due.tsx +49 -15
  36. package/src/pages/customer/subscription/change-payment.tsx +362 -0
  37. package/src/pages/customer/subscription/{update.tsx → change-plan.tsx} +26 -37
  38. package/src/pages/customer/subscription/detail.tsx +19 -7
  39. package/api/src/libs/hooks.ts +0 -25
@@ -6,12 +6,13 @@ import pick from 'lodash/pick';
6
6
  import uniq from 'lodash/uniq';
7
7
  import type { WhereOptions } from 'sequelize';
8
8
 
9
+ import { ensureStripeSubscription } from '../integrations/stripe/resource';
9
10
  import { getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
10
11
  import dayjs from '../libs/dayjs';
11
12
  import logger from '../libs/logger';
12
13
  import { isDelegationSufficientForPayment } from '../libs/payment';
13
14
  import { authenticate } from '../libs/security';
14
- import { expandLineItems, isLineItemAligned } from '../libs/session';
15
+ import { expandLineItems, getFastCheckoutAmount, isLineItemAligned } from '../libs/session';
15
16
  import { createProration, getSubscriptionCreateSetup, getSubscriptionRefundSetup } from '../libs/subscription';
16
17
  import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
17
18
  import { invoiceQueue } from '../queues/invoice';
@@ -27,6 +28,7 @@ import { Price } from '../store/models/price';
27
28
  import { PricingTable } from '../store/models/pricing-table';
28
29
  import { Product } from '../store/models/product';
29
30
  import { Refund } from '../store/models/refund';
31
+ import { SetupIntent } from '../store/models/setup-intent';
30
32
  import { Subscription, TSubscription } from '../store/models/subscription';
31
33
  import { SubscriptionItem } from '../store/models/subscription-item';
32
34
  import type { LineItem, ServiceAction, SubscriptionUpdateItem } from '../store/models/types';
@@ -379,7 +381,11 @@ router.put('/:id/recover', authPortal, async (req, res) => {
379
381
  return res.status(400).json({ error: 'Subscription not recoverable from cancellation' });
380
382
  }
381
383
 
382
- await updateStripeSubscription(doc, { cancel_at_period_end: false, cancel_at: null, canceled_at: null });
384
+ if (doc.cancel_at_period_end) {
385
+ await updateStripeSubscription(doc, { cancel_at_period_end: false });
386
+ } else {
387
+ await updateStripeSubscription(doc, { cancel_at: null });
388
+ }
383
389
 
384
390
  await doc.update({ cancel_at_period_end: false, cancel_at: 0, canceled_at: 0 });
385
391
 
@@ -873,7 +879,7 @@ router.put('/:id', authPortal, async (req, res) => {
873
879
  } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
874
880
  connectAction = 'collect';
875
881
  } else {
876
- connectAction = 'update';
882
+ connectAction = 'change-plan';
877
883
  }
878
884
  }
879
885
  }
@@ -944,7 +950,7 @@ const getUpdateTable = async (subscription: Subscription) => {
944
950
  };
945
951
 
946
952
  // Check that the subscription is upgradable
947
- router.get('/:id/update', authPortal, async (req, res) => {
953
+ router.get('/:id/change-plan', authPortal, async (req, res) => {
948
954
  try {
949
955
  const subscription = await Subscription.findByPk(req.params.id);
950
956
  if (!subscription) {
@@ -970,8 +976,8 @@ router.get('/:id/update', authPortal, async (req, res) => {
970
976
  }
971
977
  });
972
978
 
973
- // Simulate subscription update
974
- router.post('/:id/update', authPortal, async (req, res) => {
979
+ // Simulate subscription plan change
980
+ router.post('/:id/change-plan', authPortal, async (req, res) => {
975
981
  try {
976
982
  const subscription = await Subscription.findByPk(req.params.id);
977
983
  if (!subscription) {
@@ -1063,6 +1069,248 @@ router.get('/:id/proration', authPortal, async (req, res) => {
1063
1069
  }
1064
1070
  });
1065
1071
 
1072
+ // Check payment change status
1073
+ router.get('/:id/change-payment', authPortal, async (req, res) => {
1074
+ const subscription = await Subscription.findByPk(req.params.id);
1075
+ if (!subscription) {
1076
+ return res.status(404).json({ error: 'Subscription not found' });
1077
+ }
1078
+ const context = subscription.metadata.changePayment || {};
1079
+ if (!context.setup_intent_id) {
1080
+ return res.status(404).json({ error: 'Subscription change payment context not found' });
1081
+ }
1082
+
1083
+ const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
1084
+ return res.json({ subscription, setupIntent });
1085
+ });
1086
+
1087
+ // Prepare setupIntent for payment change
1088
+ router.post('/:id/change-payment', authPortal, async (req, res) => {
1089
+ try {
1090
+ const subscription = await Subscription.findByPk(req.params.id);
1091
+ if (!subscription) {
1092
+ return res.status(404).json({ error: `Subscription ${req.params.id} not found when change payment` });
1093
+ }
1094
+ if (subscription.isActive() === false) {
1095
+ return res.status(400).json({ error: `Subscription ${req.params.id} not active when change payment` });
1096
+ }
1097
+ const paymentCurrency = await PaymentCurrency.findByPk(req.body.payment_currency);
1098
+ if (!paymentCurrency) {
1099
+ return res
1100
+ .status(400)
1101
+ .json({ error: `Payment currency ${req.body.payment_currency} not found when change payment` });
1102
+ }
1103
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
1104
+ if (!paymentMethod) {
1105
+ return res
1106
+ .status(400)
1107
+ .json({ error: `Payment method ${paymentCurrency.payment_method_id} not found when change payment` });
1108
+ }
1109
+
1110
+ const customer = await Customer.findByPk(subscription.customer_id);
1111
+ if (paymentMethod.type === 'stripe') {
1112
+ await customer?.update({ address: Object.assign({}, customer.address, req.body.billing_address) });
1113
+ }
1114
+
1115
+ if (subscription.currency_id === paymentCurrency.id) {
1116
+ return res.status(400).json({ error: 'Payment currency not changed when change payment' });
1117
+ }
1118
+
1119
+ const previousPaymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1120
+ if (previousPaymentMethod?.type === 'stripe') {
1121
+ if (!subscription.payment_details?.stripe?.subscription_id) {
1122
+ return res.status(400).json({ error: 'Can not change from stripe without stripe subscription id' });
1123
+ }
1124
+ }
1125
+
1126
+ // ensure setupIntent
1127
+ const context = subscription.metadata.changePayment || {};
1128
+ let setupIntent: SetupIntent | null = null;
1129
+ if (context.setup_intent_id) {
1130
+ // should be cleared after success
1131
+ setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
1132
+ }
1133
+ // Reuse existing setupIntent if not succeeded
1134
+ if (setupIntent && setupIntent.status !== 'succeeded') {
1135
+ await setupIntent.update({
1136
+ status: 'requires_capture',
1137
+ customer_id: subscription.customer_id,
1138
+ currency_id: paymentCurrency.id,
1139
+ payment_method_id: paymentMethod.id,
1140
+ last_setup_error: null,
1141
+ });
1142
+ logger.info('setupIntent reset on subscription payment change submit', {
1143
+ subscription: subscription.id,
1144
+ intent: setupIntent.id,
1145
+ });
1146
+ } else {
1147
+ setupIntent = await SetupIntent.create({
1148
+ livemode: !!subscription.livemode,
1149
+ customer_id: subscription.customer_id,
1150
+ description: subscription.description || `payment change setup for ${subscription.id}`,
1151
+ currency_id: paymentCurrency.id,
1152
+ payment_method_id: paymentMethod.id,
1153
+ status: 'requires_payment_method',
1154
+ payment_method_types: [paymentMethod.type],
1155
+ flow_directions: ['inbound', 'outbound'],
1156
+ usage: 'off_session',
1157
+ metadata: {
1158
+ subscription_id: subscription.id,
1159
+ from_currency: subscription.currency_id,
1160
+ to_currency: paymentCurrency.id,
1161
+ from_method: subscription.default_payment_method_id,
1162
+ to_method: paymentMethod.id,
1163
+ },
1164
+ });
1165
+
1166
+ // persist setup intent id
1167
+ await subscription.update({
1168
+ metadata: { ...subscription.metadata, changePayment: { setup_intent_id: setupIntent.id } },
1169
+ });
1170
+
1171
+ logger.info('setupIntent created on subscription payment change submit', {
1172
+ subscription: subscription.id,
1173
+ intent: setupIntent.id,
1174
+ });
1175
+ }
1176
+
1177
+ // if we can complete purchase without any wallet interaction
1178
+ const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
1179
+ const lineItems = await Price.expand(
1180
+ subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity }))
1181
+ );
1182
+
1183
+ let stripeContext: any = null;
1184
+ let delegation: any = null;
1185
+ if (paymentMethod.type === 'stripe') {
1186
+ const client = paymentMethod.getStripeClient();
1187
+ let exist;
1188
+ if (subscription.payment_details?.stripe?.subscription_id) {
1189
+ exist = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id);
1190
+ }
1191
+ if (exist) {
1192
+ if (exist.status === 'paused') {
1193
+ await client.subscriptions.resume(exist.id, {
1194
+ proration_behavior: 'none',
1195
+ });
1196
+ }
1197
+ const result = await client.subscriptions.update(exist.id, {
1198
+ trial_end: subscription.current_period_end,
1199
+ proration_behavior: 'none',
1200
+ pause_collection: '',
1201
+ });
1202
+ logger.info('stripe subscription updated on subscription payment change', {
1203
+ subscription: subscription.id,
1204
+ intent: setupIntent.id,
1205
+ stripeSubscriptionId: exist.id,
1206
+ result,
1207
+ });
1208
+ await setupIntent.update({
1209
+ status: 'succeeded',
1210
+ last_setup_error: null,
1211
+ payment_method_types: [paymentMethod.type],
1212
+ });
1213
+ await subscription.update({
1214
+ currency_id: paymentCurrency.id,
1215
+ default_payment_method_id: paymentMethod.id,
1216
+ payment_settings: {
1217
+ payment_method_types: [paymentMethod.type],
1218
+ payment_method_options: {},
1219
+ },
1220
+ });
1221
+ } else {
1222
+ const settings = PaymentMethod.decryptSettings(paymentMethod.settings);
1223
+
1224
+ // changing from crypto to stripe: create/resume stripe subscription, pause crypto subscription
1225
+ const stripeSubscription = await ensureStripeSubscription(
1226
+ subscription,
1227
+ paymentMethod,
1228
+ paymentCurrency,
1229
+ lineItems,
1230
+ 0,
1231
+ subscription.current_period_end
1232
+ );
1233
+
1234
+ stripeContext = {
1235
+ type: 'subscription',
1236
+ id: stripeSubscription.id,
1237
+ // @ts-ignore
1238
+ client_secret:
1239
+ stripeSubscription.latest_invoice?.payment_intent?.client_secret ||
1240
+ stripeSubscription.pending_setup_intent?.client_secret,
1241
+ intent_type: stripeSubscription.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
1242
+ publishable_key: settings.stripe?.publishable_key,
1243
+ status: stripeSubscription.status,
1244
+ };
1245
+
1246
+ await setupIntent.update({
1247
+ setup_details: {
1248
+ stripe: {
1249
+ customer_id: stripeSubscription.customer,
1250
+ subscription_id: stripeSubscription.id,
1251
+ setup_intent_id: stripeSubscription.pending_setup_intent.id,
1252
+ },
1253
+ },
1254
+ });
1255
+ }
1256
+ } else {
1257
+ // changing from stripe to crypto: pause stripe subscription
1258
+ if (previousPaymentMethod!.type === 'stripe') {
1259
+ const client = await previousPaymentMethod?.getStripeClient();
1260
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
1261
+ const result = await client?.subscriptions.update(stripeSubscriptionId, {
1262
+ pause_collection: {
1263
+ behavior: 'void',
1264
+ },
1265
+ });
1266
+ logger.info('stripe subscription paused on payment change', {
1267
+ subscription: subscription.id,
1268
+ stripeSubscription: stripeSubscriptionId,
1269
+ result,
1270
+ });
1271
+ }
1272
+
1273
+ // changing from crypto to crypto: just update the subscription
1274
+ delegation = await isDelegationSufficientForPayment({
1275
+ paymentMethod,
1276
+ paymentCurrency,
1277
+ userDid: customer!.did,
1278
+ amount: getFastCheckoutAmount(lineItems, 'subscription', paymentCurrency.id, false),
1279
+ });
1280
+ if (delegation.sufficient) {
1281
+ await setupIntent.update({
1282
+ status: 'succeeded',
1283
+ last_setup_error: null,
1284
+ payment_method_types: [paymentMethod.type],
1285
+ });
1286
+ await subscription.update({
1287
+ currency_id: paymentCurrency.id,
1288
+ default_payment_method_id: paymentMethod.id,
1289
+ payment_settings: {
1290
+ payment_method_types: [paymentMethod.type],
1291
+ payment_method_options: {},
1292
+ },
1293
+ });
1294
+ logger.info('Subscription payment change done on delegation enough', {
1295
+ subscription: subscription.id,
1296
+ intent: setupIntent.id,
1297
+ });
1298
+ }
1299
+ }
1300
+
1301
+ return res.json({
1302
+ setupIntent,
1303
+ stripeContext,
1304
+ subscription,
1305
+ customer,
1306
+ delegation,
1307
+ });
1308
+ } catch (err) {
1309
+ console.error(err);
1310
+ return res.status(500).json({ code: err.code, error: err.message });
1311
+ }
1312
+ });
1313
+
1066
1314
  // FIXME: this should be removed in future
1067
1315
  // Clean up subscriptions that have invalid invoices and payments
1068
1316
  router.delete('/cleanup', auth, async (req, res) => {
@@ -8,7 +8,7 @@ import { sequelize } from './sequelize';
8
8
 
9
9
  const umzug = new Umzug({
10
10
  migrations: {
11
- glob: ['migrations/*.{ts,js}', { cwd: __dirname }],
11
+ glob: ['**/migrations/*.{ts,js}', { cwd: __dirname }],
12
12
  // @FIXME: @wangshijun jianchao这边的注释了才能 blocklet dev 成功
13
13
  // resolve: ({ name, path, context }) => {
14
14
  // // eslint-disable-next-line import/no-dynamic-require, global-require
@@ -5,7 +5,7 @@ import type { LiteralUnion } from 'type-fest';
5
5
 
6
6
  import { createEvent, createStatusEvent } from '../../libs/audit';
7
7
  import { createIdGenerator } from '../../libs/util';
8
- import type { PaymentError, PaymentMethodOptions } from './types';
8
+ import type { PaymentDetails, PaymentError, PaymentMethodOptions } from './types';
9
9
 
10
10
  const nextId = createIdGenerator('seti', 24);
11
11
 
@@ -49,10 +49,7 @@ export class SetupIntent extends Model<InferAttributes<SetupIntent>, InferCreati
49
49
  declare payment_method_id: string;
50
50
 
51
51
  // 3rd party payment tx hash
52
- declare setup_details?: {
53
- tx_hash?: string;
54
- payer?: string;
55
- };
52
+ declare setup_details?: PaymentDetails;
56
53
 
57
54
  // TODO: following fields not supported yet
58
55
  // application
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.159
17
+ version: 1.13.161
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.159",
3
+ "version": "1.13.161",
4
4
  "scripts": {
5
5
  "dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -13,7 +13,7 @@
13
13
  "clean": "node scripts/build-clean.js",
14
14
  "bundle": "tsc --noEmit && npm run bundle:client && npm run bundle:api",
15
15
  "bundle:client": "vite build",
16
- "bundle:api": "npm run clean && tsc -p tsconfig.api.json && blocklet bundle --zip --create-release",
16
+ "bundle:api": "npm run clean && tsc -p tsconfig.api.json && blocklet bundle --compact --external sqlite3 --create-release",
17
17
  "build": "npm run clean && tsc -p tsconfig.api.json && npm run bundle:client",
18
18
  "types": "rm -rf types && tsc -p tsconfig.types.json && rm -f ../../packages/types/lib/*.d.ts && cp -f types/store/models/*.d.ts ../../packages/types/lib",
19
19
  "deploy": "npm run bundle && blocklet deploy .blocklet/bundle",
@@ -45,20 +45,20 @@
45
45
  "@abtnode/cron": "1.16.23",
46
46
  "@arcblock/did": "^1.18.110",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.9.29",
48
+ "@arcblock/did-connect": "^2.9.39",
49
49
  "@arcblock/did-util": "^1.18.110",
50
50
  "@arcblock/jwt": "^1.18.110",
51
- "@arcblock/ux": "^2.9.29",
51
+ "@arcblock/ux": "^2.9.39",
52
52
  "@blocklet/logger": "1.16.23",
53
- "@blocklet/payment-react": "1.13.159",
53
+ "@blocklet/payment-react": "1.13.161",
54
54
  "@blocklet/sdk": "1.16.23",
55
- "@blocklet/ui-react": "^2.9.29",
56
- "@blocklet/uploader": "^0.0.73",
57
- "@mui/icons-material": "^5.15.8",
58
- "@mui/lab": "^5.0.0-alpha.164",
59
- "@mui/material": "^5.15.7",
60
- "@mui/styles": "^5.15.8",
61
- "@mui/system": "^5.15.8",
55
+ "@blocklet/ui-react": "^2.9.39",
56
+ "@blocklet/uploader": "^0.0.74",
57
+ "@mui/icons-material": "^5.15.11",
58
+ "@mui/lab": "^5.0.0-alpha.166",
59
+ "@mui/material": "^5.15.11",
60
+ "@mui/styles": "^5.15.11",
61
+ "@mui/system": "^5.15.11",
62
62
  "@ocap/asset": "^1.18.110",
63
63
  "@ocap/client": "^1.18.110",
64
64
  "@ocap/mcrypto": "^1.18.110",
@@ -110,7 +110,7 @@
110
110
  "devDependencies": {
111
111
  "@abtnode/types": "1.16.23",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@blocklet/payment-types": "1.13.159",
113
+ "@blocklet/payment-types": "1.13.161",
114
114
  "@types/cookie-parser": "^1.4.6",
115
115
  "@types/cors": "^2.8.17",
116
116
  "@types/dotenv-flow": "^3.3.3",
@@ -149,5 +149,5 @@
149
149
  "parser": "typescript"
150
150
  }
151
151
  },
152
- "gitHead": "2663776fb04b0446e2d053eedfb66d1910c507f8"
152
+ "gitHead": "55cb16d9cd06b91b949a1a1c88b14670b0c1aca8"
153
153
  }
package/src/app.tsx CHANGED
@@ -23,7 +23,8 @@ const CustomerHome = React.lazy(() => import('./pages/customer/index'));
23
23
  const CustomerInvoiceDetail = React.lazy(() => import('./pages/customer/invoice/detail'));
24
24
  const CustomerInvoicePastDue = React.lazy(() => import('./pages/customer/invoice/past-due'));
25
25
  const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/subscription/detail'));
26
- const CustomerSubscriptionUpdate = React.lazy(() => import('./pages/customer/subscription/update'));
26
+ const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
27
+ const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
27
28
 
28
29
  const theme = createTheme({
29
30
  typography: {
@@ -71,11 +72,20 @@ function App() {
71
72
  }
72
73
  />
73
74
  <Route
74
- key="customer-subscription"
75
- path="/customer/subscription/:id/update"
75
+ key="customer-subscription-change-plan"
76
+ path="/customer/subscription/:id/change-plan"
77
+ element={
78
+ <Layout>
79
+ <CustomerSubscriptionChangePlan />
80
+ </Layout>
81
+ }
82
+ />
83
+ <Route
84
+ key="customer-subscription-change-payment"
85
+ path="/customer/subscription/:id/change-payment"
76
86
  element={
77
87
  <Layout>
78
- <CustomerSubscriptionUpdate />
88
+ <CustomerSubscriptionChangePayment />
79
89
  </Layout>
80
90
  }
81
91
  />
@@ -0,0 +1,25 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Typography } from '@mui/material';
3
+ import isEmpty from 'lodash/isEmpty';
4
+ import isObject from 'lodash/isObject';
5
+
6
+ import InfoRow from '../info-row';
7
+
8
+ export default function MetadataList({ data }: { data: any }) {
9
+ const { t } = useLocaleContext();
10
+
11
+ if (isEmpty(data)) {
12
+ return <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>;
13
+ }
14
+
15
+ // skip non-string values
16
+ return (
17
+ <>
18
+ {Object.keys(data || {})
19
+ .filter((key) => isObject(data[key]) === false)
20
+ .map((key) => (
21
+ <InfoRow key={key} label={key} value={data[key]} />
22
+ ))}
23
+ </>
24
+ );
25
+ }
@@ -27,7 +27,7 @@ export default function CurrencySelect({ mode: initialMode, hasSelected, onSelec
27
27
  if (mode === 'selecting') {
28
28
  return (
29
29
  <Select value="" sx={{ width: 260 }} size="small" onChange={handleSelect}>
30
- {getSupportedPaymentMethods(settings.paymentMethods, hasSelected).map((method) => [
30
+ {getSupportedPaymentMethods(settings.paymentMethods, (x) => !hasSelected(x)).map((method) => [
31
31
  <ListSubheader key={method.id} sx={{ fontSize: '1rem', color: 'text.secondary', lineHeight: '2.5rem' }}>
32
32
  {method.name}
33
33
  </ListSubheader>,
@@ -25,7 +25,7 @@ const fetchUpdateOptions = ({ id, showUpdate }: { id: string; showUpdate: boolea
25
25
  return Promise.resolve(false);
26
26
  }
27
27
 
28
- return api.get(`/api/subscriptions/${id}/update`).then((res) => !!res.data);
28
+ return api.get(`/api/subscriptions/${id}/change-plan`).then((res) => !!res.data);
29
29
  };
30
30
 
31
31
  export function SubscriptionActionsInner({ subscription, showUpdate, onChange }: Props) {
@@ -82,9 +82,7 @@ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }:
82
82
  size="small"
83
83
  onClick={() => {
84
84
  if (action.action === 'pastDue') {
85
- navigate(
86
- `/customer/invoice/${subscription.latest_invoice_id}?action=${action.canRenew ? 'renew' : 'pay'}`
87
- );
85
+ navigate(`/customer/invoice/past-due?subscription=${subscription.id}`);
88
86
  } else {
89
87
  setState({
90
88
  action: action.action,
@@ -101,9 +99,9 @@ export function SubscriptionActionsInner({ subscription, showUpdate, onChange }:
101
99
  color="primary"
102
100
  size="small"
103
101
  onClick={() => {
104
- navigate(`/customer/subscription/${subscription.id}/update`);
102
+ navigate(`/customer/subscription/${subscription.id}/change-plan`);
105
103
  }}>
106
- {t('payment.customer.upgrade.button')}
104
+ {t('payment.customer.changePlan.button')}
107
105
  </Button>
108
106
  )}
109
107
  {subscription.service_actions?.map((x) => (
package/src/libs/util.ts CHANGED
@@ -1,11 +1,6 @@
1
1
  /* eslint-disable no-nested-ternary */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import {
4
- formatCheckoutHeadlines,
5
- formatPrice,
6
- getCheckoutAmount,
7
- getPriceCurrencyOptions,
8
- } from '@blocklet/payment-react';
3
+ import { formatCheckoutHeadlines, formatPrice, getPriceCurrencyOptions } from '@blocklet/payment-react';
9
4
  import type {
10
5
  LineItem,
11
6
  TLineItemExpanded,
@@ -14,12 +9,12 @@ import type {
14
9
  TPaymentMethodExpanded,
15
10
  TPrice,
16
11
  TProductExpanded,
12
+ TSubscriptionExpanded,
17
13
  } from '@blocklet/payment-types';
18
14
  import cloneDeep from 'lodash/cloneDeep';
19
15
  import isEqual from 'lodash/isEqual';
20
16
 
21
17
  import { t } from '../locales/index';
22
- import dayjs from './dayjs';
23
18
 
24
19
  export const formatProductPrice = (
25
20
  { prices, unit_label }: { prices: TPrice[]; unit_label: string },
@@ -63,20 +58,7 @@ export function getPriceFromProducts(products: TProductExpanded[], priceId: stri
63
58
  }
64
59
 
65
60
  export function formatPaymentLinkPricing(link: TPaymentLinkExpanded, currency: TPaymentCurrency) {
66
- const amount = getCheckoutAmount(link.line_items, currency, !!link.subscription_data?.trial_period_days);
67
- return formatCheckoutHeadlines(
68
- {
69
- mode: 'payment',
70
- status: 'open',
71
- payment_status: 'unpaid',
72
- currency,
73
- amount_total: amount.total,
74
- amount_subtotal: amount.subtotal,
75
- expires_at: dayjs().add(30, 'days').unix(),
76
- ...link,
77
- } as any,
78
- currency
79
- );
61
+ return formatCheckoutHeadlines(link.line_items, currency, link.subscription_data?.trial_period_days || 0);
80
62
  }
81
63
 
82
64
  export function getWebhookStatusColor(status: string) {
@@ -203,3 +185,7 @@ export const debounce = (fun: Function, wait: number) => {
203
185
  }, wait);
204
186
  };
205
187
  };
188
+
189
+ export function canChangePaymentMethod(subscription: TSubscriptionExpanded) {
190
+ return subscription.items.every((x) => getPriceCurrencyOptions(x.price).length > 1);
191
+ }
@@ -8,7 +8,6 @@ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/ma
8
8
  import { styled } from '@mui/system';
9
9
  import { fromUnitToToken } from '@ocap/util';
10
10
  import { useRequest, useSetState } from 'ahooks';
11
- import { isEmpty } from 'lodash';
12
11
  import { Link } from 'react-router-dom';
13
12
 
14
13
  import Copyable from '../../../../components/copyable';
@@ -19,6 +18,7 @@ import InfoRow from '../../../../components/info-row';
19
18
  import InvoiceActions from '../../../../components/invoice/action';
20
19
  import InvoiceTable from '../../../../components/invoice/table';
21
20
  import MetadataEditor from '../../../../components/metadata/editor';
21
+ import MetadataList from '../../../../components/metadata/list';
22
22
  import PaymentList from '../../../../components/payment-intent/list';
23
23
  import RefundList from '../../../../components/refund/list';
24
24
  import SectionHeader from '../../../../components/section/header';
@@ -186,15 +186,7 @@ export default function InvoiceDetail(props: { id: string }) {
186
186
  </Button>
187
187
  </SectionHeader>
188
188
  <Box className="section-body">
189
- {!state.editing.metadata &&
190
- (isEmpty(data.metadata) ? (
191
- <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
192
- ) : (
193
- Object.keys(data.metadata || {}).map((key) => (
194
- // @ts-ignore
195
- <InfoRow key={key} label={key} value={data.metadata[key]} />
196
- ))
197
- ))}
189
+ {!state.editing.metadata && <MetadataList data={data.metadata} />}
198
190
  {state.editing.metadata && (
199
191
  <MetadataEditor
200
192
  data={data}
@@ -7,7 +7,6 @@ import { ArrowBackOutlined, Edit } from '@mui/icons-material';
7
7
  import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
8
8
  import { styled } from '@mui/system';
9
9
  import { useRequest, useSetState } from 'ahooks';
10
- import { isEmpty } from 'lodash';
11
10
  import { Link } from 'react-router-dom';
12
11
 
13
12
  import Copyable from '../../../../components/copyable';
@@ -17,6 +16,7 @@ import EventList from '../../../../components/event/list';
17
16
  import InfoRow from '../../../../components/info-row';
18
17
  import InvoiceList from '../../../../components/invoice/list';
19
18
  import MetadataEditor from '../../../../components/metadata/editor';
19
+ import MetadataList from '../../../../components/metadata/list';
20
20
  import RefundList from '../../../../components/refund/list';
21
21
  import SectionHeader from '../../../../components/section/header';
22
22
  import SubscriptionActions from '../../../../components/subscription/actions';
@@ -174,14 +174,7 @@ export default function SubscriptionDetail(props: { id: string }) {
174
174
  </Button>
175
175
  </SectionHeader>
176
176
  <Box className="section-body">
177
- {!state.editing.metadata &&
178
- (isEmpty(data.metadata) ? (
179
- <Typography color="text.secondary">{t('common.metadata.empty')}</Typography>
180
- ) : (
181
- Object.keys(data.metadata || {}).map((key) => (
182
- <InfoRow key={key} label={key} value={data.metadata[key]} />
183
- ))
184
- ))}
177
+ {!state.editing.metadata && <MetadataList data={data.metadata} />}
185
178
  {state.editing.metadata && (
186
179
  <MetadataEditor
187
180
  data={data}
@@ -29,14 +29,14 @@ import SubscriptionList from '../../../../components/subscription/list';
29
29
  const fetchData = async (
30
30
  id: string
31
31
  ): Promise<{ customer: TCustomerExpanded; summary: { [key: string]: GroupedBN } }> => {
32
- const results = await Promise.all([
32
+ const [customer, summary] = await Promise.all([
33
33
  api.get(`/api/customers/${id}`).then((res) => res.data),
34
34
  api.get(`/api/customers/${id}/summary`).then((res) => res.data),
35
35
  ]);
36
36
 
37
37
  return {
38
- customer: results[0],
39
- summary: results[1],
38
+ customer,
39
+ summary,
40
40
  };
41
41
  };
42
42