@stacksjs/defaults 0.74.69 → 0.75.0

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 (40) hide show
  1. package/ai/skills/stacks-payments/SKILL.md +28 -3
  2. package/app/Actions/Auth/CreateTokenAction.ts +1 -1
  3. package/app/Actions/Auth/ListTokensAction.ts +1 -1
  4. package/app/Actions/Auth/ResendVerificationAction.ts +1 -1
  5. package/app/Actions/Auth/RevokeTokenAction.ts +1 -1
  6. package/app/Actions/Auth/VerifyRegistrationAction.ts +1 -1
  7. package/app/Actions/Billable/CheckoutAction.ts +8 -11
  8. package/app/Actions/Cms/PostStoreAction.ts +4 -1
  9. package/app/Actions/Dashboard/Auth/MeAction.ts +1 -1
  10. package/app/Actions/Dashboard/Settings/BillingShowAction.ts +1 -1
  11. package/app/Actions/Payment/CancelSubscriptionAction.ts +21 -3
  12. package/app/Actions/Payment/CreateCheckoutAction.ts +6 -1
  13. package/app/Actions/Payment/CreateInvoiceSubscription.ts +12 -1
  14. package/app/Actions/Payment/CreatePaymentIntentAction.ts +9 -4
  15. package/app/Actions/Payment/CreateSetupIntentAction.ts +6 -1
  16. package/app/Actions/Payment/CreateSubscriptionAction.ts +9 -2
  17. package/app/Actions/Payment/DeleteDefaultPaymentAction.ts +6 -1
  18. package/app/Actions/Payment/FetchActiveSubscriptionAction.ts +6 -1
  19. package/app/Actions/Payment/FetchDefaultPaymentMethodAction.ts +6 -1
  20. package/app/Actions/Payment/FetchPaymentCustomerAction.ts +8 -1
  21. package/app/Actions/Payment/FetchPaymentMethodsAction.ts +6 -1
  22. package/app/Actions/Payment/FetchTransactionHistoryAction.ts +1 -1
  23. package/app/Actions/Payment/SetDefaultPaymentAction.ts +10 -2
  24. package/app/Actions/Payment/SetUserDefaultPaymentAction.ts +8 -1
  25. package/app/Actions/Payment/StorePaymentMethodAction.ts +6 -1
  26. package/app/Actions/Payment/StoreTransactionAction.ts +19 -1
  27. package/app/Actions/Payment/UpdateCustomerAction.ts +23 -7
  28. package/app/Actions/Payment/UpdateDefaultPaymentMethodAction.ts +6 -1
  29. package/app/Actions/Payment/UpdateSubscriptionAction.ts +7 -2
  30. package/ide/vscode/package.json +1 -1
  31. package/package.json +2 -2
  32. package/resources/components/Dashboard/Auth/Login.stx +2 -2
  33. package/resources/components/Dashboard/Auth/Register.stx +4 -4
  34. package/resources/components/Dashboard/Infrastructure/LogsDashboard.stx +1 -0
  35. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +1 -0
  36. package/resources/components/Dashboard/MobileSidebar.stx +1 -0
  37. package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +2 -2
  38. package/resources/components/Dashboard/UI/Drawer.stx +1 -0
  39. package/resources/components/Dashboard/UI/Modal.stx +1 -0
  40. package/resources/plugins/preloader.ts +5 -5
@@ -429,9 +429,11 @@ Plans use `productName`, `description`, `metadata`, and a `pricing` array where
429
429
  ## User Model Requirements
430
430
  The `UserModel` must have:
431
431
  - `id`, `name`, `email`, `stripe_id` fields
432
- - `hasStripeId()` method -- returns boolean
433
432
  - `update(data)` method -- for persisting `stripe_id`
434
- - `activeSubscription()` method -- for subscription updates
433
+ - `activeSubscription()` method -- for subscription updates (bound by the `billable` trait)
434
+
435
+ There is no `user.hasStripeId()` instance method. Use
436
+ `manageCustomer.hasStripeId(user)`, which reads `stripe_id`.
435
437
 
436
438
  The framework default `storage/framework/defaults/app/Models/User.ts` sets
437
439
  `billable: false` intentionally because not every application uses payments.
@@ -439,7 +441,30 @@ Run `buddy publish:model User`, keep the override at `app/Models/User.ts`, and
439
441
  enable its `billable` trait before calling instance helpers such as
440
442
  `activeSubscription()`, `paymentMethods()`, or `createSetupIntent()`. A payment
441
443
  Action must report the missing trait clearly instead of calling an undefined
442
- method.
444
+ method. `isBillable(user)` from `@stacksjs/orm` is that check, and narrows the
445
+ user to `BillableMethods` (the instance surface, derived from
446
+ `createBillableMethods`) so no cast is needed:
447
+
448
+ ```ts
449
+ import { isBillable } from '@stacksjs/orm'
450
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
451
+
452
+ const user = await request.user()
453
+ if (!user)
454
+ return response.unauthorized('Authentication required')
455
+ if (!isBillable(user))
456
+ return response.error(BILLING_NOT_ENABLED, 503)
457
+
458
+ const customer = await user.retrieveStripeUser()
459
+ ```
460
+
461
+ The instance methods are exactly the keys of `createBillableMethods` in
462
+ `core/orm/src/traits/billable.ts`. Among them: `retrieveStripeUser()`,
463
+ `createPayment(amount, options)`, `setDefaultPaymentMethod(id)` (a number is the
464
+ local row, a string is Stripe's `pm_...` id), `deletePaymentMethod(id)`,
465
+ `syncStripeCustomerDetails(options)`, `storeTransaction(productId, options)`,
466
+ `newSubscription(type, lookupKey, options)`. Anything else is not a function at
467
+ runtime.
443
468
 
444
469
  ## Gotchas
445
470
  - Stripe API keys MUST be in `.env` as `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` -- never hardcode them in config files
@@ -39,7 +39,7 @@ export default new Action({
39
39
  return response.badRequest('At least one valid token scope is required')
40
40
 
41
41
  try {
42
- const result = await createToken(user.id, name, scopeArray, {
42
+ const result = await createToken(Number(user.id), name, scopeArray, {
43
43
  expiresInMinutes,
44
44
  withRefreshToken,
45
45
  refreshExpiresInDays: 30,
@@ -13,7 +13,7 @@ export default new Action({
13
13
  return response.unauthorized('Authentication required')
14
14
  }
15
15
 
16
- const userTokens = await tokens(user.id)
16
+ const userTokens = await tokens(Number(user.id))
17
17
 
18
18
  return response.json({
19
19
  tokens: userTokens.map(token => ({
@@ -13,7 +13,7 @@ export default new Action({
13
13
  }
14
14
 
15
15
  const { resendVerificationEmail } = await import('@stacksjs/auth')
16
- const result = await resendVerificationEmail(user)
16
+ const result = await resendVerificationEmail({ ...user, id: Number(user.id) })
17
17
 
18
18
  if (!result.success) {
19
19
  return response.json({ success: false, message: result.message }, 422)
@@ -20,7 +20,7 @@ export default new Action({
20
20
  }
21
21
 
22
22
  // Verify the token belongs to the user
23
- const userTokens = await tokens(user.id)
23
+ const userTokens = await tokens(Number(user.id))
24
24
  const tokenToRevoke = userTokens.find(t => t.id === tokenId)
25
25
 
26
26
  if (!tokenToRevoke) {
@@ -51,7 +51,7 @@ export default new Action({
51
51
  expectedRPID,
52
52
  )
53
53
 
54
- await setCurrentRegistrationOptions(user, verification)
54
+ await setCurrentRegistrationOptions({ id: Number(user.id), email: user.email }, verification)
55
55
 
56
56
  return verification
57
57
  }
@@ -1,6 +1,7 @@
1
1
  import type { UserModel } from '@stacksjs/orm'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { manageCheckout } from '@stacksjs/payments'
3
+ import { isBillable } from '@stacksjs/orm'
4
+ import { BILLING_NOT_ENABLED, manageCheckout } from '@stacksjs/payments'
4
5
  import { response } from '@stacksjs/router'
5
6
 
6
7
  export default new Action({
@@ -20,16 +21,12 @@ export default new Action({
20
21
  }
21
22
 
22
23
  // Checkout needs the billable trait, which the default User model leaves
23
- // OFF ("not every app bills through the User model"). Without it the
24
- // methods manageCheckout calls - hasStripeId() and friends - are not on
25
- // the record at all, so say which switch is missing instead of failing
26
- // deeper in with "user.hasStripeId is not a function".
27
- if (typeof (user as unknown as Partial<UserModel>).hasStripeId !== 'function') {
28
- return response.json(
29
- { message: 'Billing is not enabled. Set `traits.billable` on the User model to use checkout.' },
30
- 503,
31
- )
32
- }
24
+ // OFF ("not every app bills through the User model"), so say which switch
25
+ // is missing instead of failing deeper in. This used to ask whether
26
+ // `hasStripeId` was a function - a method nothing binds, billable or not -
27
+ // so it answered 503 even in an app that had turned billing on.
28
+ if (!isBillable(user))
29
+ return response.error(BILLING_NOT_ENABLED, 503)
33
30
 
34
31
  const session = await manageCheckout.create(user as unknown as UserModel, params)
35
32
 
@@ -18,8 +18,11 @@ export default new Action({
18
18
  if (!user)
19
19
  return response.unauthorized('Authentication required')
20
20
 
21
+ // `name` is a column of the default User model, not of every User model,
22
+ // so it is read as what it is: possibly absent. An author with no name
23
+ // falls back to the email, which every authenticated user has.
21
24
  const author = await authors.findOrCreate({
22
- name: user.name,
25
+ name: typeof user.name === 'string' && user.name !== '' ? user.name : user.email,
23
26
  email: user.email,
24
27
  })
25
28
 
@@ -72,7 +72,7 @@ export default new Action({
72
72
  const res: MeResponse = {
73
73
  user: {
74
74
  id: userId,
75
- name: user.name ?? null,
75
+ name: typeof user.name === 'string' ? user.name : null,
76
76
  email: user.email ?? null,
77
77
  },
78
78
  roles,
@@ -73,7 +73,7 @@ export default new Action({
73
73
  settleBillableRead(user, 'activeSubscription'),
74
74
  settleBillableRead(user, 'paymentMethods'),
75
75
  settleBillableRead(user, 'defaultPaymentMethod'),
76
- PaymentTransaction.where('user_id', authenticatedUser.id).orderByDesc('id').get(),
76
+ PaymentTransaction.where('user_id', Number(authenticatedUser.id)).orderByDesc('id').get(),
77
77
  ])
78
78
 
79
79
  const providerReads: SettledBillingRead[] = [
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable, SubscriptionNotOwnedError } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -6,15 +8,31 @@ export default new Action({
6
8
  description: 'Cancel Subscription for stripe',
7
9
  method: 'POST',
8
10
  async handle(request: RequestInstance) {
9
- const providerId = request.get('providerId') as string
11
+ const providerId = request.get<unknown>('providerId')
10
12
 
11
13
  const user = await request.user()
12
14
 
13
15
  if (!user)
14
16
  return response.unauthorized('Authentication required')
15
17
 
16
- const subscription = await user?.cancelSubscription(providerId)
18
+ if (!isBillable(user))
19
+ return response.error(BILLING_NOT_ENABLED, 503)
17
20
 
18
- return response.json(subscription?.paymentIntent)
21
+ if (typeof providerId !== 'string' || !providerId)
22
+ return response.json({ error: 'providerId is required' }, 422)
23
+
24
+ // Only the caller's own subscription: the trait checks ownership, and one
25
+ // 404 covers both "no such subscription" and "someone else's".
26
+ try {
27
+ // `cancelSubscription` answers `{ subscription }` and nothing else. This
28
+ // returned `.paymentIntent` off it, which is always undefined.
29
+ const { subscription } = await user.cancelSubscription(providerId)
30
+ return response.json(subscription)
31
+ }
32
+ catch (error) {
33
+ if (error instanceof SubscriptionNotOwnedError)
34
+ return response.json({ error: 'Subscription not found' }, 404)
35
+ throw error
36
+ }
19
37
  },
20
38
  })
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,10 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const checkout = await user?.checkout([
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ const checkout = await user.checkout([
15
20
  {
16
21
  priceId: 'price_1QBEfsBv6MhUdo23avVV0kqx',
17
22
  quantity: 1,
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,16 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const subscription = await user?.newSubscriptionInvoice('pro', 'stacks_pro_monthly')
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ // There is no `newSubscriptionInvoice` on a billable user, and never was, so
20
+ // this threw "is not a function". An invoiced subscription is an ordinary
21
+ // one that Stripe bills by emailing an invoice instead of charging a card.
22
+ const subscription = await user.newSubscription('pro', 'stacks_pro_monthly', {
23
+ collection_method: 'send_invoice',
24
+ days_until_due: 30,
25
+ })
15
26
 
16
27
  return response.json(subscription)
17
28
  },
@@ -1,5 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { HttpError } from '@stacksjs/error-handling'
3
+ import { isBillable } from '@stacksjs/orm'
4
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
3
5
  import { response } from '@stacksjs/router'
4
6
 
5
7
  export default new Action({
@@ -16,14 +18,17 @@ export default new Action({
16
18
  if (!user)
17
19
  return response.unauthorized('Authentication required')
18
20
 
21
+ if (!isBillable(user))
22
+ return response.error(BILLING_NOT_ENABLED, 503)
23
+
19
24
  if (!product) {
20
25
  throw new HttpError(422, 'Product not found!')
21
26
  }
22
27
 
23
- const paymentIntent = await user?.paymentIntent({
24
- // The Product model's column is `price`; `unit_price` has never been
25
- // one, so this read was undefined and the intent was created for NaN.
26
- amount: Number(product.get('price')),
28
+ // The Product model's column is `price`; `unit_price` has never been one,
29
+ // so that read was undefined and the intent was created for NaN. And the
30
+ // method is `createPayment`: `user.paymentIntent()` never existed.
31
+ const paymentIntent = await user.createPayment(Number(product.get('price')), {
27
32
  currency: 'usd',
28
33
  payment_method_types: ['card'],
29
34
  })
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,10 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const setupIntent = await user?.createSetupIntent({
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ const setupIntent = await user.createSetupIntent({
15
20
  payment_method_types: ['card', 'link', 'us_bank_account'],
16
21
  })
17
22
 
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -16,8 +18,13 @@ export default new Action({
16
18
  if (!user)
17
19
  return response.unauthorized('Authentication required')
18
20
 
19
- const subscription = await user?.newSubscription(plan, type, { description, metadata: { period } })
21
+ if (!isBillable(user))
22
+ return response.error(BILLING_NOT_ENABLED, 503)
20
23
 
21
- return response.json(subscription?.paymentIntent)
24
+ // `plan` is the subscription's name ('pro') and `type` the price lookup key
25
+ // ('stacks_pro_monthly'), which is the order `newSubscription` takes them in.
26
+ const { paymentIntent } = await user.newSubscription(plan, type, { description, metadata: { period } })
27
+
28
+ return response.json(paymentIntent)
22
29
  },
23
30
  })
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,8 +13,11 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
14
19
  const paymentMethod = Number(request.get('paymentMethod'))
15
20
 
16
- await user?.deletePaymentMethod(paymentMethod)
21
+ await user.deletePaymentMethod(paymentMethod)
17
22
  },
18
23
  })
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,10 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const subscription = await user?.activeSubscription()
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ const subscription = await user.activeSubscription()
15
20
 
16
21
  return response.json(subscription)
17
22
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,10 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const paymentMethod = await user?.defaultPaymentMethod()
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ const paymentMethod = await user.defaultPaymentMethod()
15
20
 
16
21
  return response.json(paymentMethod)
17
22
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,12 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const customer = await user?.asStripeUser()
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ // `asStripeUser()` does not exist; `retrieveStripeUser()` is the lookup,
20
+ // and answers undefined for a user with no Stripe customer yet.
21
+ const customer = await user.retrieveStripeUser()
15
22
 
16
23
  return response.json(customer)
17
24
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,7 +13,10 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const paymentMethods = await user?.paymentMethods()
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ const paymentMethods = await user.paymentMethods()
15
20
 
16
21
  return response.json(paymentMethods)
17
22
  },
@@ -11,7 +11,7 @@ export default new Action({
11
11
  if (!user)
12
12
  return response.unauthorized('Authentication required')
13
13
 
14
- const transactions = await PaymentTransaction.where('user_id', user?.id).get()
14
+ const transactions = await PaymentTransaction.where('user_id', Number(user.id)).get()
15
15
 
16
16
  return response.json(transactions)
17
17
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,9 +13,15 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const paymentId = Number(request.get('setupIntent'))
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
15
18
 
16
- const paymentMethod = await user?.setDefaultPaymentMethod(paymentId)
19
+ // The payment store posts `{ paymentId }`; this read only `setupIntent`, so
20
+ // it saw undefined and asked for payment method NaN. `setupIntent` is still
21
+ // read for any client written against the old name.
22
+ const paymentId = Number(request.get('paymentId') ?? request.get('setupIntent'))
23
+
24
+ const paymentMethod = await user.setDefaultPaymentMethod(paymentId)
17
25
 
18
26
  return response.json(paymentMethod)
19
27
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,9 +13,14 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
14
19
  const paymentId = String(request.get('setupIntent'))
15
20
 
16
- const paymentMethod = await user?.setUserDefaultPaymentMethod(paymentId)
21
+ // A string id is Stripe's own `pm_...`, which `setDefaultPaymentMethod`
22
+ // resolves on Stripe's side. `setUserDefaultPaymentMethod()` never existed.
23
+ const paymentMethod = await user.setDefaultPaymentMethod(paymentId)
17
24
 
18
25
  return response.json(paymentMethod)
19
26
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,9 +13,12 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
14
19
  const paymentIntent = request.get('setupIntent') as string
15
20
 
16
- const paymentMethod = await user?.addPaymentMethod(paymentIntent)
21
+ const paymentMethod = await user.addPaymentMethod(paymentIntent)
17
22
 
18
23
  return response.json(paymentMethod)
19
24
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -13,7 +15,23 @@ export default new Action({
13
15
  if (!user)
14
16
  return response.unauthorized('Authentication required')
15
17
 
16
- const transaction = await user?.storeTransaction(productId)
18
+ if (!isBillable(user))
19
+ return response.error(BILLING_NOT_ENABLED, 503)
20
+
21
+ // A transaction records a payment that already happened, so it needs the
22
+ // Stripe id of that payment and the card brand it was made with. The call
23
+ // used to pass neither, and threw reading `options.description`.
24
+ const providerId = String(request.get('providerId') ?? '')
25
+ const brand = String(request.get('brand') ?? '')
26
+
27
+ if (!providerId || !brand)
28
+ return response.error('A transaction needs the `providerId` of the Stripe payment and the card `brand`.', 422)
29
+
30
+ const transaction = await user.storeTransaction(productId, {
31
+ provider_id: providerId,
32
+ brand,
33
+ description: request.get('description') ? String(request.get('description')) : undefined,
34
+ })
17
35
 
18
36
  return response.json(transaction)
19
37
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,13 +13,27 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
14
- const customer = await user?.syncStripeCustomerDetails({ address: {
15
- line1: '123 Elm St',
16
- city: 'Austin',
17
- state: 'TX',
18
- postal_code: '78701',
19
- country: 'US',
20
- } })
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
19
+ // The address comes from the request. It was a hard-coded placeholder, so
20
+ // every customer synced through here was moved to the same street in
21
+ // Austin.
22
+ const field = (key: string): string | undefined => {
23
+ const value = request.get(key)
24
+ return typeof value === 'string' && value !== '' ? value : undefined
25
+ }
26
+
27
+ const customer = await user.syncStripeCustomerDetails({
28
+ address: {
29
+ line1: field('line1'),
30
+ line2: field('line2'),
31
+ city: field('city'),
32
+ state: field('state'),
33
+ postal_code: field('postal_code'),
34
+ country: field('country'),
35
+ },
36
+ })
21
37
 
22
38
  return response.json(customer)
23
39
  },
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -11,8 +13,11 @@ export default new Action({
11
13
  if (!user)
12
14
  return response.unauthorized('Authentication required')
13
15
 
16
+ if (!isBillable(user))
17
+ return response.error(BILLING_NOT_ENABLED, 503)
18
+
14
19
  const paymentMethod = Number(request.get('paymentMethod'))
15
20
 
16
- await user?.setDefaultPaymentMethod(paymentMethod)
21
+ await user.setDefaultPaymentMethod(paymentMethod)
17
22
  },
18
23
  })
@@ -1,4 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { isBillable } from '@stacksjs/orm'
3
+ import { BILLING_NOT_ENABLED } from '@stacksjs/payments'
2
4
  import { response } from '@stacksjs/router'
3
5
 
4
6
  export default new Action({
@@ -15,8 +17,11 @@ export default new Action({
15
17
  if (!user)
16
18
  return response.unauthorized('Authentication required')
17
19
 
18
- const subscription = await user?.updateSubscription(plan, type, { description })
20
+ if (!isBillable(user))
21
+ return response.error(BILLING_NOT_ENABLED, 503)
19
22
 
20
- return response.json(subscription?.paymentIntent)
23
+ const { paymentIntent } = await user.updateSubscription(plan, type, { description })
24
+
25
+ return response.json(paymentIntent)
21
26
  },
22
27
  })
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.69",
5
+ "version": "0.75.0",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.69",
5
+ "version": "0.75.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.74.69",
58
+ "@stacksjs/mobile": "^0.75.0",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -81,7 +81,7 @@ function handleSubmit(): void {
81
81
  label="Email address"
82
82
  placeholder="you@example.com"
83
83
  v-model:value="email"
84
- @blur="validateEmail"
84
+ @blur="validateEmail()"
85
85
  :error="fieldError('email')"
86
86
  required
87
87
  :disabled="isLoading()"
@@ -94,7 +94,7 @@ function handleSubmit(): void {
94
94
  label="Password"
95
95
  placeholder="Enter your password"
96
96
  v-model:value="password"
97
- @blur="validatePassword"
97
+ @blur="validatePassword()"
98
98
  :error="fieldError('password')"
99
99
  required
100
100
  :disabled="isLoading()"
@@ -144,7 +144,7 @@ function handleSubmit(): void {
144
144
  label="Email address"
145
145
  placeholder="you@example.com"
146
146
  v-model:value="email"
147
- @blur="validateEmail"
147
+ @blur="validateEmail()"
148
148
  :error="fieldError('email')"
149
149
  required
150
150
  :disabled="isLoading()"
@@ -158,8 +158,8 @@ function handleSubmit(): void {
158
158
  label="Password"
159
159
  placeholder="At least 8 characters"
160
160
  v-model:value="password"
161
- @input="validatePassword"
162
- @blur="validatePassword"
161
+ @input="validatePassword()"
162
+ @blur="validatePassword()"
163
163
  :error="fieldError('password')"
164
164
  required
165
165
  :disabled="isLoading()"
@@ -185,7 +185,7 @@ function handleSubmit(): void {
185
185
  label="Confirm password"
186
186
  placeholder="Confirm your password"
187
187
  v-model:value="passwordConfirmation"
188
- @input="validateConfirmation"
188
+ @input="validateConfirmation()"
189
189
  :error="fieldError('passwordConfirmation')"
190
190
  required
191
191
  :disabled="isLoading()"
@@ -5,6 +5,7 @@ import type {
5
5
  DashboardLogType,
6
6
  } from '../../../../app/Actions/Dashboard/Infrastructure/log-dashboard'
7
7
  import { dashboardApi } from '../../../../functions/dashboard-api'
8
+ import { debounce } from '@stacksjs/browser/utils/debounce'
8
9
 
9
10
  interface LogPagination {
10
11
  page: number
@@ -1,5 +1,6 @@
1
1
  <script client>
2
2
  import { kanbanStore } from '~/storage/framework/defaults/views/dashboard/stores/kanban'
3
+ import { useTimeoutFn } from '@stacksjs/composables'
3
4
 
4
5
  const kanban = kanbanStore
5
6
  const openCard = derived(() => kanban.openCard())
@@ -1,4 +1,5 @@
1
1
  <script client>
2
+ import { useScrollLock } from '@stacksjs/composables'
2
3
  const isOpen = state(false)
3
4
  const isDesktop = useMediaQuery('(min-width: 1024px)')
4
5
  const scrollLocked = useScrollLock()
@@ -449,7 +449,7 @@ onMount(() => {
449
449
  confirmLabel="Remove member"
450
450
  :busy="Boolean(busyMemberId())"
451
451
  @close="memberToRemove.set(null)"
452
- @confirm="removeMember"
452
+ @confirm="removeMember()"
453
453
  />
454
454
 
455
455
  <ConfirmDialog
@@ -459,5 +459,5 @@ onMount(() => {
459
459
  confirmLabel="Revoke invitation"
460
460
  :busy="Boolean(busyInvitationId())"
461
461
  @close="invitationToRevoke.set(null)"
462
- @confirm="revokeInvitation"
462
+ @confirm="revokeInvitation()"
463
463
  />
@@ -17,6 +17,7 @@ const sizeClasses = {
17
17
  </script>
18
18
 
19
19
  <script client>
20
+ import { useScrollLock } from '@stacksjs/composables'
20
21
  const open = useReactiveProp('isOpen', false)
21
22
  const liveClosable = useReactiveProp('closable', true)
22
23
  const liveTitle = useReactiveProp('title', '')
@@ -41,6 +41,7 @@ const positionClasses = {
41
41
  }
42
42
  </script>
43
43
  <script client>
44
+ import { useScrollLock } from '@stacksjs/composables'
44
45
  const open = useReactiveProp('isOpen', false)
45
46
  const liveClosable = useReactiveProp('closable', true)
46
47
  const liveTitle = useReactiveProp('title', '')
@@ -278,7 +278,7 @@ export async function loadAutoImports() {
278
278
  // Skip default exports and protected globals
279
279
  if (name === 'default' || protectedGlobals.has(name)) continue
280
280
  if (typeof value !== 'undefined') {
281
- (globalThis as any)[name] = value
281
+ Reflect.set(globalThis, name, value)
282
282
  }
283
283
  }
284
284
  } catch {
@@ -298,7 +298,7 @@ export async function loadAutoImports() {
298
298
  // Skip default exports and protected globals
299
299
  if (name === 'default' || protectedGlobals.has(name)) continue
300
300
  if (typeof value !== 'undefined') {
301
- (globalThis as any)[name] = value
301
+ Reflect.set(globalThis, name, value)
302
302
  }
303
303
  }
304
304
  } catch {
@@ -331,7 +331,7 @@ export async function loadAutoImports() {
331
331
  try {
332
332
  const module = await import(file)
333
333
  if (module.default) {
334
- (globalThis as any)[modelName] = module.default
334
+ Reflect.set(globalThis, modelName, module.default)
335
335
  loadedModels.add(modelName)
336
336
  }
337
337
  } catch {
@@ -362,7 +362,7 @@ export async function loadAutoImports() {
362
362
  try {
363
363
  const module = await import(file)
364
364
  if (module.default) {
365
- (globalThis as any)[jobName] = module.default
365
+ Reflect.set(globalThis, jobName, module.default)
366
366
  loadedJobs.add(jobName)
367
367
  }
368
368
  } catch {
@@ -397,7 +397,7 @@ export async function loadAutoImports() {
397
397
  try {
398
398
  const module = await import(file)
399
399
  if (module.default) {
400
- (globalThis as any)[controllerName] = module.default
400
+ Reflect.set(globalThis, controllerName, module.default)
401
401
  loadedControllers.add(controllerName)
402
402
  }
403
403
  } catch {