payment-kit 1.13.270 → 1.13.271

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.
@@ -1,5 +1,4 @@
1
1
  import { isEthereumDid, isValid } from '@arcblock/did';
2
- // import pick from 'lodash/pick';
3
2
  import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
4
3
  import merge from 'lodash/merge';
5
4
 
@@ -59,7 +58,7 @@ export async function mintNftForCheckoutSession(id: string) {
59
58
  ]);
60
59
 
61
60
  const preMint = preMintFromFactory({
62
- factory: formatFactoryState(factoryState),
61
+ factory: formatFactoryState(factoryState as any),
63
62
  inputs: inputs || {},
64
63
  owner: nftOwner,
65
64
  issuer: { wallet, name: appState.moniker },
@@ -2,6 +2,7 @@ import pick from 'lodash/pick';
2
2
  import type Stripe from 'stripe';
3
3
 
4
4
  import logger from '../../../libs/logger';
5
+ import { finalizeStripeSubscriptionUpdate } from '../../../libs/subscription';
5
6
  import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
6
7
 
7
8
  export async function handleStripeSubscriptionSucceed(subscription: Subscription, status: string) {
@@ -83,7 +84,24 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
83
84
  if (subscription.status === 'trialing' && event.data.object.status === 'active') {
84
85
  fields.push('status');
85
86
  }
86
- await subscription.update(pick(event.data.object, fields));
87
+
88
+ await finalizeStripeSubscriptionUpdate({
89
+ subscription,
90
+ updates: pick(event.data.object, fields),
91
+ items: (event.data.object.items.data || []).map((x: any) => ({
92
+ id: x.metadata?.id,
93
+ price_id: x.price.metadata?.id,
94
+ stripe_id: x.id,
95
+ stripe_price_id: x.price.id,
96
+ stripe_subscription_id: x.subscription,
97
+ metadata: x.metadata,
98
+ quantity: x.quantity,
99
+ billing_thresholds: x.billing_thresholds,
100
+ // discounts: x.discounts,
101
+ // tax_rates: x.tax_rates,
102
+ })),
103
+ });
104
+
87
105
  return;
88
106
  }
89
107
 
@@ -12,6 +12,7 @@ import {
12
12
  InvoiceItem,
13
13
  Lock,
14
14
  PaymentCurrency,
15
+ PaymentMethod,
15
16
  Price,
16
17
  PriceRecurring,
17
18
  Subscription,
@@ -21,6 +22,7 @@ import {
21
22
  UsageRecord,
22
23
  } from '../store/models';
23
24
  import dayjs from './dayjs';
25
+ import env from './env';
24
26
  import logger from './logger';
25
27
  import { getPriceCurrencyOptions, getPriceUintAmountByCurrency, getRecurringPeriod } from './session';
26
28
  import { getConnectQueryParam } from './util';
@@ -228,7 +230,6 @@ export async function createProration(
228
230
  setup: ReturnType<typeof getSubscriptionCreateSetup>,
229
231
  anchor: number
230
232
  ) {
231
- // FIXME: should we enforce cycle invoices here?
232
233
  const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
233
234
  if (!lastInvoice) {
234
235
  throw new Error('Subscription should have latest invoice when create proration');
@@ -249,9 +250,11 @@ export async function createProration(
249
250
 
250
251
  // 2. calculate proration args based on the filtered invoice items
251
252
  const precision = 10000;
252
- const prorationStart = lastInvoice.period_start;
253
- const prorationEnd = lastInvoice.period_end;
253
+ const isInvoicePeriodValid = lastInvoice.period_start < lastInvoice.period_end;
254
+ const prorationStart = isInvoicePeriodValid ? lastInvoice.period_start : subscription.current_period_start;
255
+ const prorationEnd = isInvoicePeriodValid ? lastInvoice.period_end : subscription.current_period_end;
254
256
  if (anchor > prorationEnd) {
257
+ logger.warn('try to create proration with invalid arguments', { anchor, prorationStart, prorationEnd });
255
258
  throw new Error('Subscription proration anchor should not be larger than prorationEnd');
256
259
  }
257
260
 
@@ -524,6 +527,69 @@ export async function finalizeSubscriptionUpdate({
524
527
  logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
525
528
  }
526
529
 
530
+ export async function finalizeStripeSubscriptionUpdate({
531
+ subscription,
532
+ items,
533
+ updates,
534
+ }: {
535
+ subscription: Subscription;
536
+ items: any[];
537
+ updates: any;
538
+ }) {
539
+ if (isEmpty(updates)) {
540
+ logger.info('subscription update aborted', { subscription: subscription.id, updates });
541
+ return;
542
+ }
543
+
544
+ if (subscription.pending_update) {
545
+ // handle subscription item remove
546
+ // this must be done before update/create
547
+ const localItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
548
+ const deletedItems = localItems.filter((x) => items.some((y) => y.id === x.id) === false).map((x) => x.id);
549
+ if (deletedItems.length) {
550
+ await UsageRecord.destroy({ where: { subscription_item_id: deletedItems } });
551
+ logger.info('subscription item usage cleared on update', { subscription: subscription.id, deletedItems });
552
+ await SubscriptionItem.destroy({ where: { id: deletedItems } });
553
+ logger.info('subscription item deleted on update', { subscription: subscription.id, deletedItems });
554
+ }
555
+
556
+ // handle subscription item update/create
557
+ for (const item of items) {
558
+ if (item.id) {
559
+ // remote item is associated with local item
560
+ await SubscriptionItem.update(pick(item, ['quantity', 'billing_thresholds']), { where: { id: item.id } });
561
+ logger.info('subscription item synced on update', { subscription: subscription.id, item });
562
+ } else {
563
+ // remote item not associated with local item
564
+ const created = await SubscriptionItem.create({
565
+ price_id: item.price_id as string,
566
+ quantity: item.quantity as number,
567
+ livemode: subscription.livemode,
568
+ subscription_id: subscription.id,
569
+ metadata: {
570
+ stripe_id: item.stripe_id,
571
+ stripe_subscription_id: item.stripe_subscription_id,
572
+ },
573
+ });
574
+ logger.info('subscription item mirrored on update', { subscription: subscription.id, item });
575
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
576
+ const client = method!.getStripeClient();
577
+ const result = await client.subscriptionItems.update(item.stripe_id, {
578
+ metadata: { appPid: env.appPid, id: created.id },
579
+ });
580
+ logger.info('subscription item related on mirror', { subscription: subscription.id, item, result });
581
+ }
582
+ }
583
+
584
+ const releaseAt = subscription.current_period_end;
585
+ await Lock.acquire(`${subscription.id}-change-plan`, releaseAt);
586
+ logger.info('subscription plan change lock acquired on finalize', { subscription: subscription.id, releaseAt });
587
+ }
588
+
589
+ logger.info('subscription update finalized', { subscription: subscription.id, updates, items });
590
+ await subscription.update({ ...updates, pending_update: null });
591
+ }
592
+
527
593
  export async function onSubscriptionUpdateConnected(subscriptionId: string) {
528
594
  const subscription = await Subscription.findByPk(subscriptionId);
529
595
  if (!subscription) {
@@ -263,7 +263,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
263
263
  raw.nft_mint_status = 'disabled';
264
264
  }
265
265
  } catch {
266
- // Do nothing
266
+ // Do nothing
267
267
  }
268
268
 
269
269
  const session = await CheckoutSession.create(raw as any);
@@ -5,7 +5,7 @@ import Joi from 'joi';
5
5
  import pick from 'lodash/pick';
6
6
  import uniq from 'lodash/uniq';
7
7
 
8
- import { ensureStripeSubscription } from '../integrations/stripe/resource';
8
+ import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
9
9
  import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery } from '../libs/api';
10
10
  import dayjs from '../libs/dayjs';
11
11
  import logger from '../libs/logger';
@@ -552,7 +552,7 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
552
552
  }
553
553
 
554
554
  const updatedExpanded = await Price.expand(updatedItems as LineItem[]);
555
- const newItems: any[] = [...existingExpanded, ...addedExpanded, ...updatedExpanded];
555
+ const newItems: TLineItemExpanded[] = [...existingExpanded, ...addedExpanded, ...updatedExpanded];
556
556
  if (newItems.length === 0) {
557
557
  throw new Error('Subscription should have at least one subscription item');
558
558
  }
@@ -675,6 +675,8 @@ router.put('/:id', authPortal, async (req, res) => {
675
675
  // handle subscription item changes
676
676
  let connectAction = '';
677
677
  if (Array.isArray(value.items) && value.items.length > 0) {
678
+ const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
679
+
678
680
  if (subscription.isActive() === false) {
679
681
  throw new Error('Updating subscription item not allowed for inactive subscriptions');
680
682
  }
@@ -682,10 +684,6 @@ router.put('/:id', authPortal, async (req, res) => {
682
684
  throw new Error('Updating subscription item not allowed for scheduled-to-cancel subscriptions');
683
685
  }
684
686
 
685
- if (paymentMethod.type === 'stripe') {
686
- throw new Error('Updating subscription item not allowed for subscriptions paid with stripe');
687
- }
688
-
689
687
  const locked = await Lock.isLocked(`${subscription.id}-change-plan`);
690
688
  if (locked) {
691
689
  throw new Error('Updating subscription item not allowed now until next billing cycle');
@@ -697,167 +695,232 @@ router.put('/:id', authPortal, async (req, res) => {
697
695
  value.items
698
696
  );
699
697
 
700
- // update subscription period settings
701
- // HINT: if we are adding new items, we need to reset the anchor to now
702
- const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0);
703
- if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
704
- updates.pending_invoice_item_interval = setup.recurring;
705
- updates.current_period_start = setup.period.start;
706
- updates.current_period_end = setup.period.end;
707
- updates.billing_cycle_anchor = setup.cycle.anchor;
708
- logger.info('subscription updates on reset anchor', { subscription: req.params.id, updates });
709
- }
710
-
711
- // handle proration
712
- const prorationBehavior = updates.proration_behavior || subscription.proration_behavior || 'none';
713
- if (prorationBehavior === 'create_prorations') {
714
- // 0. cleanup open invoices
715
- if (subscription.pending_update?.updates?.latest_invoice_id) {
716
- await cleanupInvoiceAndItems(subscription.pending_update?.updates?.latest_invoice_id);
717
- // @ts-ignore
718
- await subscription.update({ pending_update: null });
719
- }
698
+ const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
699
+ if (paymentMethod.type === 'stripe' && stripeSubscriptionId) {
700
+ await ensureStripeCustomer(customer, paymentMethod);
720
701
 
721
- // 1. create proration
722
- const { lastInvoice, due, newCredit, appliedCredit, prorations } = await createProration(
723
- subscription,
724
- setup,
725
- dayjs().unix()
702
+ const addedStripeItems = await Promise.all(
703
+ addedItems.map(async (x) => {
704
+ const price = await Price.findByPk(x.price_id);
705
+ const stripePrice = await ensureStripePrice(price!, paymentMethod, paymentCurrency);
706
+ return { price: stripePrice.id, quantity: x.quantity };
707
+ })
708
+ );
709
+ const updatedStripeItems = await Promise.all(
710
+ updatedItems.map(async (x) => {
711
+ const price = newItems.find((y) => y.price_id === x.price_id);
712
+ const item = await SubscriptionItem.findByPk(x.id);
713
+ return { id: item!.metadata.stripe_id, price: price!.metadata?.stripe_id, quantity: x.quantity };
714
+ })
715
+ );
716
+ const deletedStripeItems = await Promise.all(
717
+ deletedItems.map(async (x) => {
718
+ const item = await SubscriptionItem.findByPk(x.id);
719
+ return { id: item!.metadata.stripe_id, deleted: true, clear_usage: x.clear_usage };
720
+ })
726
721
  );
727
722
 
728
- // 2. create new invoice: amount according to new subscription items
729
- // 3. create new invoice items: amount according to new subscription items
730
- const result = await ensureInvoiceAndItems({
731
- customer,
732
- currency: paymentCurrency,
733
- subscription,
734
- trialing: false,
735
- metered: false,
736
- lineItems: newItems,
737
- applyCredit: false,
738
- props: {
739
- status: 'draft',
740
- livemode: subscription.livemode,
741
- description: 'Subscription update',
742
- statement_descriptor: lastInvoice.statement_descriptor,
743
- period_start: setup.period.start,
744
- period_end: setup.period.end,
745
- auto_advance: true,
746
- billing_reason: 'subscription_update',
747
- total: setup.amount.setup,
748
- currency_id: paymentCurrency.id,
749
- default_payment_method_id: subscription.default_payment_method_id,
750
- custom_fields: lastInvoice.custom_fields || [],
751
- footer: lastInvoice.footer || '',
752
- } as Invoice,
723
+ const stripeItems = [...addedStripeItems, ...updatedStripeItems, ...deletedStripeItems].filter(Boolean);
724
+ logger.info('stripe subscription update attempt', {
725
+ subscription: req.params.id,
726
+ stripeSubscriptionId,
727
+ addedStripeItems,
728
+ updatedStripeItems,
729
+ deletedStripeItems,
753
730
  });
754
- const { invoice } = result;
755
- updates.latest_invoice_id = invoice.id;
756
- logger.info('subscription update invoice created', { subscription: req.params.id, invoice: invoice.id });
757
-
758
- // 4. create proration invoice items: amount according to proration amount
759
- const prorationInvoiceItems = await Promise.all(
760
- prorations.map((x: any) =>
761
- InvoiceItem.create({
762
- ...x,
763
- livemode: subscription.livemode,
764
- currency_id: subscription.currency_id,
765
- customer_id: customer.id,
766
- price_id: x.price_id,
767
- invoice_id: invoice.id,
768
- subscription_id: subscription.id,
769
- subscription_item_id: x.subscription_item_id,
770
- discountable: false,
771
- discounts: [],
772
- discount_amounts: [],
773
- metadata: {},
774
- })
775
- )
776
- );
777
- logger.info('subscription proration invoice items created', {
731
+
732
+ await subscription.update({
733
+ pending_update: {
734
+ expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
735
+ updates,
736
+ addedItems,
737
+ deletedItems,
738
+ updatedItems,
739
+ },
740
+ });
741
+
742
+ const client = paymentMethod.getStripeClient();
743
+ const result = await client.subscriptions.update(stripeSubscriptionId, {
744
+ proration_behavior: prorationBehavior as any,
745
+ metadata: updates.metadata,
746
+ description: updates.description,
747
+ items: stripeItems,
748
+ });
749
+ logger.info('stripe subscription update done', {
778
750
  subscription: req.params.id,
779
- items: prorationInvoiceItems.map((x) => x.id),
751
+ stripeSubscriptionId,
752
+ prorationBehavior,
753
+ result,
780
754
  });
755
+ } else {
756
+ // update subscription period settings
757
+ // HINT: if we are adding new items, we need to reset the anchor to now
758
+ const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0);
759
+ if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
760
+ updates.pending_invoice_item_interval = setup.recurring;
761
+ updates.current_period_start = setup.period.start;
762
+ updates.current_period_end = setup.period.end;
763
+ updates.billing_cycle_anchor = setup.cycle.anchor;
764
+ logger.info('subscription updates on reset anchor', { subscription: req.params.id, updates });
765
+ }
781
766
 
782
- // 5. check do we need to connect
783
- let hasNext = true;
784
- if (due === '0') {
785
- hasNext = false;
786
- } else {
787
- const delegation = await isDelegationSufficientForPayment({
788
- paymentMethod,
789
- paymentCurrency,
790
- userDid: customer.did,
791
- amount: setup.amount.setup,
767
+ // handle proration
768
+ if (prorationBehavior === 'create_prorations') {
769
+ // 0. cleanup open invoices
770
+ if (subscription.pending_update?.updates?.latest_invoice_id) {
771
+ await cleanupInvoiceAndItems(subscription.pending_update?.updates?.latest_invoice_id);
772
+ // @ts-ignore
773
+ await subscription.update({ pending_update: null });
774
+ }
775
+
776
+ // 1. create proration
777
+ const { lastInvoice, due, newCredit, appliedCredit, prorations } = await createProration(
778
+ subscription,
779
+ setup,
780
+ dayjs().unix()
781
+ );
782
+
783
+ // 2. create new invoice: amount according to new subscription items
784
+ // 3. create new invoice items: amount according to new subscription items
785
+ const result = await ensureInvoiceAndItems({
786
+ customer,
787
+ currency: paymentCurrency,
788
+ subscription,
789
+ trialing: false,
790
+ metered: false,
791
+ lineItems: newItems,
792
+ applyCredit: false,
793
+ props: {
794
+ status: 'draft',
795
+ livemode: subscription.livemode,
796
+ description: 'Subscription update',
797
+ statement_descriptor: lastInvoice.statement_descriptor,
798
+ period_start: setup.period.start,
799
+ period_end: setup.period.end,
800
+ auto_advance: true,
801
+ billing_reason: 'subscription_update',
802
+ total: setup.amount.setup,
803
+ currency_id: paymentCurrency.id,
804
+ default_payment_method_id: subscription.default_payment_method_id,
805
+ custom_fields: lastInvoice.custom_fields || [],
806
+ footer: lastInvoice.footer || '',
807
+ } as Invoice,
808
+ });
809
+ const { invoice } = result;
810
+ updates.latest_invoice_id = invoice.id;
811
+ logger.info('subscription update invoice created', { subscription: req.params.id, invoice: invoice.id });
812
+
813
+ // 4. create proration invoice items: amount according to proration amount
814
+ const prorationInvoiceItems = await Promise.all(
815
+ prorations.map((x: any) =>
816
+ InvoiceItem.create({
817
+ ...x,
818
+ livemode: subscription.livemode,
819
+ currency_id: subscription.currency_id,
820
+ customer_id: customer.id,
821
+ price_id: x.price_id,
822
+ invoice_id: invoice.id,
823
+ subscription_id: subscription.id,
824
+ subscription_item_id: x.subscription_item_id,
825
+ discountable: false,
826
+ discounts: [],
827
+ discount_amounts: [],
828
+ metadata: {},
829
+ })
830
+ )
831
+ );
832
+ logger.info('subscription proration invoice items created', {
833
+ subscription: req.params.id,
834
+ items: prorationInvoiceItems.map((x) => x.id),
792
835
  });
793
- if (delegation.sufficient) {
836
+
837
+ // 5. check do we need to connect
838
+ let hasNext = true;
839
+ if (due === '0') {
794
840
  hasNext = false;
795
- } else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
796
- throw new Error('Subscription update can only be done when you do have connected DID Wallet');
797
- } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
798
- // FIXME: this is not supported at frontend
799
- connectAction = 'collect';
800
841
  } else {
801
- connectAction = 'change-plan';
842
+ const delegation = await isDelegationSufficientForPayment({
843
+ paymentMethod,
844
+ paymentCurrency,
845
+ userDid: customer.did,
846
+ amount: setup.amount.setup,
847
+ });
848
+ if (delegation.sufficient) {
849
+ hasNext = false;
850
+ } else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
851
+ throw new Error('Subscription update can only be done when you do have connected DID Wallet');
852
+ } else if (['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) {
853
+ // FIXME: this is not supported at frontend
854
+ connectAction = 'collect';
855
+ } else {
856
+ connectAction = 'change-plan';
857
+ }
802
858
  }
803
- }
804
859
 
805
- // 6. adjust invoice total
806
- await invoice.update({
807
- status: 'open',
808
- amount_due: due,
809
- amount_remaining: due,
810
- });
860
+ // 6. adjust invoice total
861
+ await invoice.update({
862
+ status: 'open',
863
+ amount_due: due,
864
+ amount_remaining: due,
865
+ });
811
866
 
812
- // 7. wait for succeed
813
- if (hasNext) {
814
- await subscription.update({
815
- pending_update: {
816
- expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
817
- updates,
867
+ // 7. wait for succeed
868
+ if (hasNext) {
869
+ await subscription.update({
870
+ pending_update: {
871
+ expires_at: dayjs().unix() + 30 * 60, // after 30 minutes
872
+ updates,
873
+ appliedCredit,
874
+ newCredit,
875
+ addedItems,
876
+ deletedItems,
877
+ updatedItems,
878
+ },
879
+ });
880
+ logger.info('subscription update invoice wait for connect', {
881
+ subscription: subscription.id,
882
+ invoice: invoice.id,
883
+ });
884
+ } else {
885
+ await invoiceQueue.pushAndWait({
886
+ id: invoice.id,
887
+ job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
888
+ });
889
+ logger.info('subscription update invoice processed', {
890
+ subscription: subscription.id,
891
+ invoice: invoice.id,
892
+ });
893
+
894
+ // check if we have succeeded
895
+ await Promise.all([invoice.reload(), subscription.reload()]);
896
+
897
+ if (invoice.status === 'paid') {
898
+ await subscriptionQueue.delete(subscription.id);
899
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
900
+ } else {
901
+ throw new Error('Subscription update invoice failed to advance');
902
+ }
903
+
904
+ await finalizeSubscriptionUpdate({
905
+ subscription,
906
+ customer,
907
+ invoice,
908
+ paymentCurrency,
818
909
  appliedCredit,
819
910
  newCredit,
820
911
  addedItems,
821
912
  deletedItems,
822
913
  updatedItems,
823
- },
824
- });
825
- logger.info('subscription update invoice wait for connect', {
826
- subscription: subscription.id,
827
- invoice: invoice.id,
828
- });
829
- } else {
830
- await invoiceQueue.pushAndWait({
831
- id: invoice.id,
832
- job: { invoiceId: invoice.id, retryOnError: false, waitForPayment: true },
833
- });
834
- logger.info('subscription update invoice processed', { subscription: subscription.id, invoice: invoice.id });
835
-
836
- // check if we have succeeded
837
- await Promise.all([invoice.reload(), subscription.reload()]);
838
-
839
- if (invoice.status === 'paid') {
840
- await subscriptionQueue.delete(subscription.id);
841
- await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
842
- } else {
843
- throw new Error('Subscription update invoice failed to advance');
914
+ updates,
915
+ });
844
916
  }
845
-
846
- await finalizeSubscriptionUpdate({
847
- subscription,
848
- customer,
849
- invoice,
850
- paymentCurrency,
851
- appliedCredit,
852
- newCredit,
853
- addedItems,
854
- deletedItems,
855
- updatedItems,
856
- updates,
857
- });
858
917
  }
859
918
  }
860
919
  } else if (req.body.billing_cycle_anchor === 'now') {
920
+ if (paymentMethod?.type === 'stripe') {
921
+ return res.status(400).json({ error: 'Update billing_cycle_anchor not supported for stripe subscriptions' });
922
+ }
923
+
861
924
  if (subscription.isActive() === false) {
862
925
  throw new Error('Updating billing_cycle_anchor not allowed for inactive subscriptions');
863
926
  }
@@ -1007,11 +1070,6 @@ router.get('/:id/change-plan', authPortal, async (req, res) => {
1007
1070
  return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1008
1071
  }
1009
1072
 
1010
- const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1011
- if (paymentMethod?.type === 'stripe') {
1012
- return res.status(400).json({ error: 'Update is not supported for subscriptions paid with stripe' });
1013
- }
1014
-
1015
1073
  const table = await getUpdateTable(subscription);
1016
1074
  return res.json(table);
1017
1075
  } catch (err) {
@@ -1038,11 +1096,6 @@ router.post('/:id/change-plan', authPortal, async (req, res) => {
1038
1096
  return res.status(400).json({ error: 'Subscription plan change is not allowed until next billing cycle' });
1039
1097
  }
1040
1098
 
1041
- const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
1042
- if (paymentMethod?.type === 'stripe') {
1043
- return res.status(400).json({ error: 'Update is not supported for subscriptions paid with stripe' });
1044
- }
1045
-
1046
1099
  const { error } = updateSchema.validate(req.body);
1047
1100
  if (error) {
1048
1101
  return res.status(400).json({ error: `Subscription update request invalid: ${error.message}` });
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.270
17
+ version: 1.13.271
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.270",
3
+ "version": "1.13.271",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -43,32 +43,32 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@abtnode/cron": "1.16.26",
46
- "@arcblock/did": "^1.18.121",
46
+ "@arcblock/did": "^1.18.123",
47
47
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
48
- "@arcblock/did-connect": "^2.9.89",
49
- "@arcblock/did-util": "^1.18.121",
50
- "@arcblock/jwt": "^1.18.121",
51
- "@arcblock/ux": "^2.9.89",
52
- "@arcblock/validator": "^1.18.121",
48
+ "@arcblock/did-connect": "^2.9.90",
49
+ "@arcblock/did-util": "^1.18.123",
50
+ "@arcblock/jwt": "^1.18.123",
51
+ "@arcblock/ux": "^2.9.90",
52
+ "@arcblock/validator": "^1.18.123",
53
53
  "@blocklet/logger": "1.16.26",
54
- "@blocklet/payment-react": "1.13.270",
54
+ "@blocklet/payment-react": "1.13.271",
55
55
  "@blocklet/sdk": "1.16.26",
56
- "@blocklet/ui-react": "^2.9.89",
56
+ "@blocklet/ui-react": "^2.9.90",
57
57
  "@blocklet/uploader": "^0.1.6",
58
- "@mui/icons-material": "^5.15.18",
58
+ "@mui/icons-material": "^5.15.19",
59
59
  "@mui/lab": "^5.0.0-alpha.170",
60
- "@mui/material": "^5.15.18",
61
- "@mui/styles": "^5.15.18",
60
+ "@mui/material": "^5.15.19",
61
+ "@mui/styles": "^5.15.19",
62
62
  "@mui/system": "^5.15.15",
63
- "@ocap/asset": "^1.18.121",
64
- "@ocap/client": "^1.18.121",
65
- "@ocap/mcrypto": "^1.18.121",
66
- "@ocap/util": "^1.18.121",
67
- "@ocap/wallet": "^1.18.121",
63
+ "@ocap/asset": "^1.18.123",
64
+ "@ocap/client": "^1.18.123",
65
+ "@ocap/mcrypto": "^1.18.123",
66
+ "@ocap/util": "^1.18.123",
67
+ "@ocap/wallet": "^1.18.123",
68
68
  "@react-pdf/renderer": "^3.4.4",
69
69
  "@stripe/react-stripe-js": "^2.7.1",
70
70
  "@stripe/stripe-js": "^2.4.0",
71
- "ahooks": "^3.7.11",
71
+ "ahooks": "^3.8.0",
72
72
  "axios": "^0.27.2",
73
73
  "body-parser": "^1.20.2",
74
74
  "cls-hooked": "^4.2.2",
@@ -78,7 +78,7 @@
78
78
  "date-fns": "^3.6.0",
79
79
  "dayjs": "^1.11.11",
80
80
  "dotenv-flow": "^3.3.0",
81
- "ethers": "^6.12.1",
81
+ "ethers": "^6.13.0",
82
82
  "express": "^4.19.2",
83
83
  "express-async-errors": "^3.1.1",
84
84
  "express-history-api-fallback": "^2.2.1",
@@ -98,7 +98,7 @@
98
98
  "react": "^18.3.1",
99
99
  "react-dom": "^18.3.1",
100
100
  "react-error-boundary": "^4.0.13",
101
- "react-hook-form": "^7.51.4",
101
+ "react-hook-form": "^7.51.5",
102
102
  "react-international-phone": "^3.1.2",
103
103
  "react-router-dom": "^6.23.1",
104
104
  "recharts": "^2.12.7",
@@ -109,22 +109,22 @@
109
109
  "stripe": "^13.11.0",
110
110
  "typewriter-effect": "^2.21.0",
111
111
  "ufo": "^1.5.3",
112
- "umzug": "^3.8.0",
112
+ "umzug": "^3.8.1",
113
113
  "use-bus": "^2.5.2",
114
114
  "validator": "^13.12.0"
115
115
  },
116
116
  "devDependencies": {
117
117
  "@abtnode/types": "1.16.26",
118
118
  "@arcblock/eslint-config-ts": "^0.3.0",
119
- "@blocklet/payment-types": "1.13.270",
119
+ "@blocklet/payment-types": "1.13.271",
120
120
  "@types/cookie-parser": "^1.4.7",
121
121
  "@types/cors": "^2.8.17",
122
122
  "@types/dotenv-flow": "^3.3.3",
123
123
  "@types/express": "^4.17.21",
124
- "@types/node": "^18.19.33",
125
- "@types/react": "^18.3.2",
124
+ "@types/node": "^18.19.34",
125
+ "@types/react": "^18.3.3",
126
126
  "@types/react-dom": "^18.3.0",
127
- "@vitejs/plugin-react": "^4.2.1",
127
+ "@vitejs/plugin-react": "^4.3.0",
128
128
  "bumpp": "^8.2.1",
129
129
  "cross-env": "^7.0.3",
130
130
  "eslint": "^8.57.0",
@@ -135,11 +135,11 @@
135
135
  "npm-run-all": "^4.1.5",
136
136
  "prettier": "^2.8.8",
137
137
  "prettier-plugin-import-sort": "^0.0.7",
138
- "ts-jest": "^29.1.2",
138
+ "ts-jest": "^29.1.4",
139
139
  "ts-node": "^10.9.2",
140
- "type-fest": "^4.18.2",
140
+ "type-fest": "^4.19.0",
141
141
  "typescript": "^4.9.5",
142
- "vite": "^5.2.11",
142
+ "vite": "^5.2.12",
143
143
  "vite-plugin-blocklet": "^0.7.9",
144
144
  "vite-plugin-node-polyfills": "^0.21.0",
145
145
  "vite-plugin-svgr": "^4.2.0",
@@ -155,5 +155,5 @@
155
155
  "parser": "typescript"
156
156
  }
157
157
  },
158
- "gitHead": "99484b4bdf4b1bba8c9dc0e20764ad0e4217d82f"
158
+ "gitHead": "cf476c72b4c54fd56881e03828e4b2c764e76e84"
159
159
  }
@@ -214,7 +214,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
214
214
  size="small"
215
215
  sx={{ width: INPUT_WIDTH }}
216
216
  error={!!getFieldState(fieldName).error}
217
- helperText={getFieldState(fieldName).error?.message}
217
+ helperText={getFieldState(fieldName).error?.message as string}
218
218
  InputProps={{
219
219
  endAdornment: (
220
220
  <InputAdornment position="end">
@@ -27,7 +27,7 @@ import { useRequest, useSetState } from 'ahooks';
27
27
  import pWaitFor from 'p-wait-for';
28
28
  import { useEffect, useState } from 'react';
29
29
  import { Controller, FormProvider, useForm, useFormContext, useWatch } from 'react-hook-form';
30
- import { useParams } from 'react-router-dom';
30
+ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
31
31
  import { joinURL } from 'ufo';
32
32
 
33
33
  import SectionHeader from '../../../components/section/header';
@@ -75,9 +75,10 @@ const waitForCheckoutComplete = async (sessionId: string) => {
75
75
 
76
76
  function CustomerSubscriptionChangePayment({ subscription, customer, onComplete }: Props) {
77
77
  const { t } = useLocaleContext();
78
- const { settings } = usePaymentContext();
78
+ const navigate = useNavigate();
79
+ const [searchParams] = useSearchParams();
80
+ const { settings, connect } = usePaymentContext();
79
81
  const { control, setValue, handleSubmit } = useFormContext();
80
- const { connect } = usePaymentContext();
81
82
 
82
83
  const [state, setState] = useSetState<{
83
84
  submitting: boolean;
@@ -115,12 +116,23 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
115
116
  const method = settings.paymentMethods.find((x: any) => x.id === selectedMethodId) as TPaymentMethod;
116
117
  const changed = selectedCurrencyId !== subscription.currency_id;
117
118
 
119
+ const handleCompleted = async () => {
120
+ await onComplete();
121
+
122
+ const redirectUrl = searchParams.get('redirectUrl');
123
+ if (redirectUrl) {
124
+ window.location.href = redirectUrl;
125
+ } else {
126
+ navigate(`/customer/subscription/${subscription.id}`);
127
+ }
128
+ };
129
+
118
130
  const handleConnected = async () => {
119
131
  try {
120
132
  await waitForCheckoutComplete(subscription.id);
121
133
  setState({ paid: true, paying: false });
122
134
  Toast.success(t('payment.customer.changePayment.completed', { time: subscription.current_period_end }));
123
- await onComplete();
135
+ await handleCompleted();
124
136
  } catch (err) {
125
137
  Toast.error(formatError(err));
126
138
  } finally {
@@ -202,7 +214,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
202
214
  <Stack
203
215
  direction="row"
204
216
  alignItems="center"
205
- sx={{ fontWeight: 'normal', mt: 2 }}
217
+ sx={{ fontWeight: 'normal', mt: 2, cursor: 'pointer' }}
206
218
  onClick={() => goBackOrFallback(`/customer/subscription/${subscription.id}`)}>
207
219
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
208
220
  <SubscriptionDescription subscription={subscription} variant="h5" />