payment-kit 1.18.4 → 1.18.6

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import env from '@blocklet/sdk/lib/env';
4
4
 
5
+ import Stripe from 'stripe';
5
6
  import logger from '../../libs/logger';
6
7
  import { STRIPE_API_VERSION, STRIPE_ENDPOINT, STRIPE_EVENTS } from '../../libs/util';
7
8
  import { PaymentMethod } from '../../store/models';
@@ -48,3 +49,29 @@ export async function ensureWebhookRegistered() {
48
49
  }
49
50
  }
50
51
  }
52
+
53
+ // validate stripe keys
54
+ export async function validateStripeKeys(secretKey: string) {
55
+ try {
56
+ const stripe = new Stripe(secretKey, { apiVersion: STRIPE_API_VERSION });
57
+ await stripe.accounts.retrieve();
58
+ return true;
59
+ } catch (error) {
60
+ logger.error('Invalid Stripe API keys:', error);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ // cleanup old stripe webhook
66
+ export async function cleanupStripeWebhook(stripe: Stripe) {
67
+ try {
68
+ const { data } = await stripe.webhookEndpoints.list({ limit: 100 });
69
+ const existingWebhook = data.find((webhook) => webhook.metadata?.appPid === env.appPid);
70
+ if (existingWebhook) {
71
+ await stripe.webhookEndpoints.del(existingWebhook.id);
72
+ logger.info('stripe webhook deleted', { id: existingWebhook.id });
73
+ }
74
+ } catch (err) {
75
+ logger.error('Failed to clean old webhook:', err);
76
+ }
77
+ }
@@ -27,10 +27,10 @@ export default {
27
27
 
28
28
  onConnect: async (args: CallbackArgs) => {
29
29
  const { userDid, userPk, extraParams } = args;
30
- const { checkoutSessionId, connectedDid } = extraParams;
30
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
31
31
  const { paymentIntent, paymentCurrency, paymentMethod } = await ensurePaymentIntent(
32
32
  checkoutSessionId,
33
- connectedDid || userDid
33
+ connectedDid || sessionUserDid || userDid
34
34
  );
35
35
  if (!paymentIntent) {
36
36
  throw new Error('Payment intent not found');
@@ -87,10 +87,10 @@ export default {
87
87
 
88
88
  onAuth: async (args: CallbackArgs) => {
89
89
  const { request, userDid, claims, extraParams } = args;
90
- const { checkoutSessionId, connectedDid } = extraParams;
90
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
91
91
  const { checkoutSession, customer, paymentIntent, paymentMethod } = await ensurePaymentIntent(
92
92
  checkoutSessionId,
93
- connectedDid || userDid
93
+ connectedDid || sessionUserDid || userDid
94
94
  );
95
95
  if (!paymentIntent) {
96
96
  throw new Error('Payment intent not found');
@@ -30,10 +30,10 @@ export default {
30
30
  },
31
31
  onConnect: async (args: CallbackArgs) => {
32
32
  const { userDid, userPk, extraParams } = args;
33
- const { checkoutSessionId, connectedDid } = extraParams;
33
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
34
34
  const { paymentMethod, paymentCurrency, checkoutSession, subscription, customer } = await ensureSetupIntent(
35
35
  checkoutSessionId,
36
- connectedDid || userDid
36
+ connectedDid || sessionUserDid || userDid
37
37
  );
38
38
  if (!subscription) {
39
39
  throw new Error('Subscription for checkoutSession not found');
@@ -123,9 +123,9 @@ export default {
123
123
  },
124
124
  onAuth: async (args: CallbackArgs) => {
125
125
  const { request, userDid, userPk, claims, extraParams } = args;
126
- const { checkoutSessionId, connectedDid } = extraParams;
126
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
127
127
  const { setupIntent, checkoutSession, paymentMethod, subscription, invoice, paymentCurrency, customer } =
128
- await ensureSetupIntent(checkoutSessionId, connectedDid || userDid);
128
+ await ensureSetupIntent(checkoutSessionId, connectedDid || sessionUserDid || userDid);
129
129
 
130
130
  if (!subscription) {
131
131
  throw new Error('Subscription for checkoutSession not found');
@@ -31,10 +31,10 @@ export default {
31
31
  },
32
32
  onConnect: async (args: CallbackArgs) => {
33
33
  const { userDid, userPk, extraParams } = args;
34
- const { checkoutSessionId, connectedDid } = extraParams;
34
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
35
35
  const { checkoutSession, paymentMethod, paymentCurrency, subscription, customer } = await ensurePaymentIntent(
36
36
  checkoutSessionId,
37
- connectedDid || userDid
37
+ connectedDid || sessionUserDid || userDid
38
38
  );
39
39
  if (!subscription) {
40
40
  throw new Error('Subscription for checkoutSession not found');
@@ -123,10 +123,10 @@ export default {
123
123
  },
124
124
  onAuth: async (args: CallbackArgs) => {
125
125
  const { request, userDid, userPk, claims, extraParams } = args;
126
- const { checkoutSessionId, connectedDid } = extraParams;
126
+ const { checkoutSessionId, connectedDid, sessionUserDid } = extraParams;
127
127
  const { checkoutSession, customer, paymentMethod, subscription, paymentCurrency } = await ensurePaymentIntent(
128
128
  checkoutSessionId,
129
- connectedDid || userDid
129
+ connectedDid || sessionUserDid || userDid
130
130
  );
131
131
 
132
132
  if (!subscription) {
@@ -7,7 +7,7 @@ import { InferAttributes, Op, WhereOptions } from 'sequelize';
7
7
  import cloneDeep from 'lodash/cloneDeep';
8
8
  import merge from 'lodash/merge';
9
9
  import { Joi } from '@arcblock/validator';
10
- import { ensureWebhookRegistered } from '../integrations/stripe/setup';
10
+ import { ensureWebhookRegistered, cleanupStripeWebhook, validateStripeKeys } from '../integrations/stripe/setup';
11
11
  import logger from '../libs/logger';
12
12
  import { authenticate } from '../libs/security';
13
13
  import { PaymentCurrency } from '../store/models/payment-currency';
@@ -51,6 +51,10 @@ router.post('/', auth, async (req, res) => {
51
51
  if (!raw.settings.stripe?.secret_key) {
52
52
  return res.status(400).json({ error: 'stripe secret key is required' });
53
53
  }
54
+ const isValid = await validateStripeKeys(raw.settings.stripe.secret_key);
55
+ if (!isValid) {
56
+ return res.status(400).json({ error: 'Invalid Stripe API keys' });
57
+ }
54
58
 
55
59
  raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['stripe']) as PaymentMethodSettings;
56
60
  raw.logo = getUrl('/methods/stripe.png');
@@ -295,7 +299,7 @@ router.put('/:id', auth, async (req, res) => {
295
299
  if ('logo' in method.dataValues || raw.logo !== undefined) {
296
300
  updateData.logo = raw.logo ?? method.logo;
297
301
  }
298
- const updateSettings = 'settings' in req.body ? req.body.settings : null;
302
+ let updateSettings = 'settings' in req.body ? req.body.settings : null;
299
303
  if (EVM_CHAIN_TYPES.includes(method.type as string) && updateSettings) {
300
304
  const paymentType = method.type as EVMChainType;
301
305
  if (!updateSettings[paymentType]?.api_host) {
@@ -322,7 +326,29 @@ router.put('/:id', auth, async (req, res) => {
322
326
  }
323
327
  updateData.settings = pick(PaymentMethod.encryptSettings(updateSettings), [paymentType]) as PaymentMethodSettings;
324
328
  }
329
+ if (method.type === 'stripe') {
330
+ if (!updateSettings.stripe?.publishable_key) {
331
+ return res.status(400).json({ error: 'stripe publishable key is required' });
332
+ }
333
+ if (!updateSettings.stripe?.secret_key) {
334
+ return res.status(400).json({ error: 'stripe secret key is required' });
335
+ }
336
+ if (method.settings?.stripe?.secret_key !== updateSettings.stripe.secret_key) {
337
+ const isValid = await validateStripeKeys(updateSettings.stripe.secret_key);
338
+ if (!isValid) {
339
+ return res.status(400).json({ error: 'Invalid Stripe API keys' });
340
+ }
341
+ updateSettings.stripe.webhook_signing_secret = null;
342
+ updateSettings = PaymentMethod.encryptSettings(updateSettings);
343
+ cleanupStripeWebhook(method.getStripeClient());
344
+ }
345
+ updateData.settings = pick(updateSettings, ['stripe']) as PaymentMethodSettings;
346
+ }
347
+
325
348
  const updatedMethod = await method.update(updateData);
349
+ if (method.type === 'stripe') {
350
+ ensureWebhookRegistered().catch(console.error);
351
+ }
326
352
  return res.json(updatedMethod);
327
353
  } catch (err) {
328
354
  return res.status(400).json({ error: err.message });
@@ -175,15 +175,21 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
175
175
  throw new Error('payment method config insufficient for stripe');
176
176
  }
177
177
 
178
- if (stripeClients.has(this.id)) {
179
- return stripeClients.get(this.id) as Stripe;
178
+ const { secret_key: secretKey } = PaymentMethod.decryptSettings(this.settings).stripe || {};
179
+ if (!secretKey) {
180
+ throw new Error('stripe secret key is required');
180
181
  }
182
+ const clientKey = `${this.id}:${secretKey}`;
183
+ const existingClient = stripeClients.get(clientKey) as Stripe;
181
184
 
182
- try {
183
- const settings = PaymentMethod.decryptSettings(this.settings);
184
- const client = new Stripe(settings.stripe?.secret_key as string, { apiVersion: STRIPE_API_VERSION });
185
- stripeClients.set(this.id, client);
185
+ if (existingClient) {
186
+ return existingClient;
187
+ }
186
188
 
189
+ try {
190
+ stripeClients.delete(this.id);
191
+ const client = new Stripe(secretKey, { apiVersion: STRIPE_API_VERSION });
192
+ stripeClients.set(clientKey, client);
187
193
  return client as Stripe;
188
194
  } catch (e) {
189
195
  return {} as Stripe;
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.18.4
17
+ version: 1.18.6
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.18.4",
3
+ "version": "1.18.6",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -53,11 +53,11 @@
53
53
  "@arcblock/validator": "^1.19.9",
54
54
  "@blocklet/js-sdk": "^1.16.38",
55
55
  "@blocklet/logger": "^1.16.38",
56
- "@blocklet/payment-react": "1.18.4",
56
+ "@blocklet/payment-react": "1.18.6",
57
57
  "@blocklet/sdk": "^1.16.38",
58
58
  "@blocklet/ui-react": "^2.11.34",
59
- "@blocklet/uploader": "^0.1.66",
60
- "@blocklet/xss": "^0.1.22",
59
+ "@blocklet/uploader": "^0.1.67",
60
+ "@blocklet/xss": "^0.1.23",
61
61
  "@mui/icons-material": "^5.16.6",
62
62
  "@mui/lab": "^5.0.0-alpha.173",
63
63
  "@mui/material": "^5.16.6",
@@ -121,7 +121,7 @@
121
121
  "devDependencies": {
122
122
  "@abtnode/types": "^1.16.38",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.4",
124
+ "@blocklet/payment-types": "1.18.6",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "003d3b762bc9e3e224e4814dea95ff02d014868d"
170
+ "gitHead": "784ba57910d780a44d0792ef503b666ee8f8d9c3"
171
171
  }
@@ -140,7 +140,7 @@ export function SubscriptionActionsInner({
140
140
  const { t, locale } = useLocaleContext();
141
141
  const { reset, getValues } = useFormContext();
142
142
  const navigate = useNavigate();
143
- const { connect } = usePaymentContext();
143
+ const { connect, session } = usePaymentContext();
144
144
  const [subs, setSubscription] = useState(subscription);
145
145
  const { checkUnpaidInvoices } = useUnpaidInvoicesCheckForSubscription(subscription.id, true);
146
146
  const action = getSubscriptionAction(subs, actionProps ?? {});
@@ -239,7 +239,7 @@ export function SubscriptionActionsInner({
239
239
  saveConnect: false,
240
240
  action: 'delegation',
241
241
  prefix: joinURL(getPrefix(), '/api/did'),
242
- extraParams: { subscriptionId: subscription.id },
242
+ extraParams: { subscriptionId: subscription.id, sessionUserDid: session.user.did },
243
243
  onSuccess: () => {
244
244
  connect.close();
245
245
  Toast.success(t('customer.delegation.success'));
@@ -286,7 +286,7 @@ export function SubscriptionActionsInner({
286
286
  saveConnect: false,
287
287
  action: 'overdraft-protection',
288
288
  prefix: joinURL(getPrefix(), '/api/did'),
289
- extraParams: { subscriptionId: subscription.id, amount },
289
+ extraParams: { subscriptionId: subscription.id, amount, sessionUserDid: session.user.did },
290
290
  onSuccess: () => {
291
291
  connect.close();
292
292
  Toast.success(t('customer.overdraftProtection.settingSuccess'));
@@ -73,6 +73,9 @@ export default function PaymentMethodEdit({ onClose, value }: { onClose: () => v
73
73
  'settings.base.explorer_host',
74
74
  'settings.ethereum.api_host',
75
75
  'settings.ethereum.explorer_host',
76
+ 'settings.stripe.publishable_key',
77
+ 'settings.stripe.secret_key',
78
+ 'settings.stripe.dashboard',
76
79
  ]}
77
80
  />
78
81
  </FormProvider>
@@ -53,7 +53,7 @@ export default function CustomerSubscriptionChangePlan() {
53
53
  const navigate = useNavigate();
54
54
  const { id } = useParams() as { id: string };
55
55
  const { t, locale } = useLocaleContext();
56
- const { connect } = usePaymentContext();
56
+ const { connect, session } = usePaymentContext();
57
57
  const [searchParams] = useSearchParams();
58
58
  const redirectUrl = searchParams.get('redirectUrl');
59
59
  const confirmRef = useRef<HTMLButtonElement>(null);
@@ -164,6 +164,7 @@ export default function CustomerSubscriptionChangePlan() {
164
164
  extraParams: {
165
165
  invoiceId: result.data.pending_update?.updates?.latest_invoice_id,
166
166
  subscriptionId: result.data.id,
167
+ sessionUserDid: session?.user?.did,
167
168
  },
168
169
  onSuccess: () => {
169
170
  setState({ paid: true, paying: false });