@stacksjs/defaults 0.75.81 → 0.75.83
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.
- package/ai/skills/stacks-commerce/SKILL.md +3 -2
- package/ai/skills/stacks-payments/SKILL.md +55 -50
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/project/bun-version +1 -0
- package/resources/components/NativeComponents.test.ts +1 -1
- package/resources/components/NativeNavBar.stx +4 -1
- package/resources/layouts/404.stx +3 -3
- package/resources/views/errors/404.stx +1 -1
|
@@ -230,11 +230,12 @@ straight-line distance by a detour factor and returns `null` for a stationary
|
|
|
230
230
|
courier rather than `Infinity`.
|
|
231
231
|
|
|
232
232
|
## Integration with Payments
|
|
233
|
-
Commerce
|
|
233
|
+
Commerce takes payments through `@stacksjs/payments`, on whichever provider `config.payment.driver` names:
|
|
234
234
|
```typescript
|
|
235
235
|
import { Payment } from '@stacksjs/payments'
|
|
236
|
-
await Payment.charge(customer, order.totalAmount, paymentMethodId)
|
|
236
|
+
await Payment.charge(customer, { amount: order.totalAmount, currency: 'usd' }, paymentMethodId, { reference: `order-${order.id}` })
|
|
237
237
|
```
|
|
238
|
+
The provider's webhook reaches orders through `POST /webhooks/payments` (see `stacks-payments`).
|
|
238
239
|
|
|
239
240
|
## Dashboard Routes
|
|
240
241
|
All commerce models have dashboard views at `/dashboard/commerce/*`.
|
|
@@ -23,7 +23,7 @@ Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01
|
|
|
23
23
|
- The Stripe driver delegates to the existing billable modules, so idempotency keys and `stripe_id` handling are unchanged.
|
|
24
24
|
- Webhook route: the `payments` default route bundle (mounted with the `commerce` feature) serves `POST /webhooks/payments` through `orders.receivePaymentWebhook(request)`: 401 `missing-config` while the driver's secret is unset (`WebhookNotConfiguredError`), 400 `invalid-signature` (`WebhookSignatureError`), otherwise applies the events and answers with `acknowledgeWebhook()`.
|
|
25
25
|
- Commerce: `orders.handleCommercePaymentEvent(event)` from `@stacksjs/commerce` applies `payment.succeeded` / `payment.failed` (reason into `payments.failure_reason`) / `refund.succeeded` (added to `refund_amount`; `refunded` + order REFUNDED only when it covers the payment, else `partiallyRefunded`) to the order, deduplicated in `payment_webhook_events` (the `PaymentWebhookEvent` model) in the same transaction.
|
|
26
|
-
-
|
|
26
|
+
- The Payment facade's neutral functions go through this driver; its Stripe-only functions and everything Stripe-level below return Stripe objects.
|
|
27
27
|
|
|
28
28
|
## Key Paths
|
|
29
29
|
- Core package: `storage/framework/core/payments/src/`
|
|
@@ -44,87 +44,91 @@ Full Stripe integration via the Payment facade. Uses Stripe API version `2026-01
|
|
|
44
44
|
|
|
45
45
|
## Payment Facade
|
|
46
46
|
|
|
47
|
-
The `Payment` object aggregates
|
|
47
|
+
The `Payment` object aggregates the payment operations; every function is also a standalone export. It has two halves (`storage/framework/core/payments/src/payment.ts`):
|
|
48
|
+
|
|
49
|
+
- **Provider-neutral** - through `paymentDriver()`, so they work on Stripe and Adyen alike. Money is `{ amount, currency }` in minor units; results are the driver's shapes.
|
|
50
|
+
- **Stripe only** - Stripe's parameters and types, and `PaymentUnsupportedError` (naming the operation) under any other driver: `changeSubscription`, `updateCustomer`, `deleteCustomer`, `addPaymentMethod`, `setDefaultPaymentMethod`, `createSetupIntent`, `getInvoices`, `createInvoice`, `payInvoice`, `createProduct`, `getPrice`, `listProducts`, `createCoupon`, `createPromoCode`, `validatePromoCode`, `billingPortal`. The check is `assertStripeDriver(operation)`, shared with the billable trait.
|
|
51
|
+
|
|
52
|
+
Something Stripe-specific the neutral half cannot express (a Connect destination charge's `payment_intent_data.transfer_data`, `prorate`/`invoice_now` on a cancellation, customer create params) goes through the `manage*` modules or the raw `stripe` client.
|
|
48
53
|
|
|
49
54
|
### Charges
|
|
50
55
|
|
|
51
56
|
```typescript
|
|
52
57
|
import { Payment } from '@stacksjs/payments'
|
|
53
58
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
59
|
+
// Charge a stored payment method, server side
|
|
60
|
+
const result = await Payment.charge(user, { amount: 2999, currency: 'usd' }, 'pm_xxx', { reference: 'order-1' })
|
|
56
61
|
|
|
57
|
-
//
|
|
58
|
-
const
|
|
62
|
+
// A payment the browser completes: result.clientConfirmation is a Stripe client secret or an Adyen session
|
|
63
|
+
const pending = await Payment.createPayment(user, { amount: 2999, currency: 'usd' })
|
|
59
64
|
|
|
60
|
-
// Refund -- partial
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// Refund -- full (omit amount)
|
|
64
|
-
const refund = await Payment.refund('pi_xxx')
|
|
65
|
+
// Refund -- partial, or full when the amount is left out
|
|
66
|
+
const partial = await Payment.refund('pi_xxx', { amount: { amount: 1000, currency: 'usd' } })
|
|
67
|
+
const full = await Payment.refund('pi_xxx')
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
The `manageCharge` module also exposes `findPayment(id)` which retrieves a PaymentIntent by ID, returning `null` on failure.
|
|
70
|
+
`PaymentResult` has `id`, `status` (`succeeded`, `processing`, `requires_action`, `requires_payment_method`, `failed`, `canceled`), `amount`, `clientConfirmation`, `redirectUrl` and `raw` (the provider's object; strip it before a browser sees it). The `manageCharge` module is the Stripe-level API (`findPayment(id)` and the PaymentIntent params).
|
|
70
71
|
|
|
71
72
|
### Checkout Sessions
|
|
72
73
|
|
|
73
74
|
```typescript
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
{ price: 'price_xxx', quantity: 1 }
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
const session = await Payment.checkout(user, {
|
|
76
|
+
mode: 'payment',
|
|
77
|
+
lines: [{ price: 'price_xxx', quantity: 1 }],
|
|
78
|
+
successUrl: 'https://app.example.com/success',
|
|
79
|
+
cancelUrl: 'https://app.example.com/cancel',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// One price in subscription mode
|
|
83
|
+
const upgrade = await Payment.subscriptionCheckout(user, 'price_xxx', {
|
|
84
|
+
successUrl: 'https://app.example.com/welcome',
|
|
85
|
+
cancelUrl: 'https://app.example.com/pricing',
|
|
86
|
+
metadata: { user_id: String(user.id) },
|
|
82
87
|
})
|
|
83
88
|
```
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
Send the payer to `session.url`. `trialDays` starts a subscription checkout with a trial. `reference` and `metadata` are copied onto the payment or subscription the checkout creates. Return URLs must be on the app's origin.
|
|
86
91
|
|
|
87
92
|
### Subscriptions
|
|
88
93
|
|
|
89
94
|
```typescript
|
|
90
|
-
//
|
|
95
|
+
// Subscribe to a price (a Stripe lookup key); `type` names it locally, `default` unless given
|
|
91
96
|
const sub = await Payment.subscribe(user, 'premium-monthly')
|
|
92
97
|
|
|
93
|
-
// Cancel
|
|
94
|
-
|
|
98
|
+
// Cancel now
|
|
99
|
+
await Payment.cancelSubscription('sub_xxx')
|
|
95
100
|
|
|
96
|
-
//
|
|
97
|
-
|
|
101
|
+
// Stop renewing: entitled until the period ends, recorded in the row's ends_at
|
|
102
|
+
await Payment.cancelSubscription('sub_xxx', { atPeriodEnd: true })
|
|
98
103
|
|
|
99
|
-
//
|
|
104
|
+
// The provider's subscriptions, every status
|
|
105
|
+
const all = await Payment.subscriptions(user)
|
|
106
|
+
|
|
107
|
+
// Entitlement, from the local subscriptions table
|
|
100
108
|
const hasActive = await Payment.hasActiveSubscription(user, 'default')
|
|
101
109
|
|
|
102
|
-
//
|
|
110
|
+
// Stripe only: swap the subscription item's price
|
|
103
111
|
const changed = await Payment.changeSubscription(user, 'enterprise-monthly')
|
|
104
112
|
```
|
|
105
113
|
|
|
106
|
-
`subscribe()`
|
|
114
|
+
`subscribe()` resolves the price by lookup key, creates the subscription with `payment_behavior: 'allow_incomplete'` and `expand: ['latest_invoice.confirmation_secret']` (an incomplete subscription's summary carries that as `clientConfirmation`), and stores it in `subscriptions`. Dates are stored as timestamps from the subscription item's `current_period_end`, where Stripe keeps it since API 2025-03-31.basil.
|
|
107
115
|
|
|
108
116
|
`isValid()` returns `true` if the subscription status is `'active'` or `'trialing'`. `isIncomplete()` checks for `'incomplete'` status.
|
|
109
117
|
|
|
110
|
-
`
|
|
111
|
-
|
|
112
|
-
`update()` retrieves the active subscription via `user.activeSubscription()`, finds the new price by lookup key, updates the subscription item, and stores the updated price in the database.
|
|
118
|
+
`cancelSubscription(id, immediately)` is gone: its `false` also cancelled at once, only with a proration. For Stripe's `prorate` / `invoice_now`, call `manageSubscription.cancel(id, params)`.
|
|
113
119
|
|
|
114
120
|
### Customers
|
|
115
121
|
|
|
116
122
|
```typescript
|
|
117
|
-
//
|
|
118
|
-
const customer = await Payment.getOrCreateCustomer(user
|
|
123
|
+
// The provider's customer, created on first use: { id, email, name }
|
|
124
|
+
const customer = await Payment.getOrCreateCustomer(user)
|
|
119
125
|
|
|
120
|
-
//
|
|
126
|
+
// Stripe only
|
|
121
127
|
const updated = await Payment.updateCustomer(user, { name: 'Jane' })
|
|
122
|
-
|
|
123
|
-
// Delete from Stripe and clear stripe_id on user model
|
|
124
128
|
const deleted = await Payment.deleteCustomer(user)
|
|
125
129
|
```
|
|
126
130
|
|
|
127
|
-
`createOrGetStripeUser()` checks `user.stripe_id` first. If the user has one, it retrieves the customer from Stripe. If the customer was deleted (404 or `deleted: true`), it creates a new one. On creation, it auto-fills `name` and `email` from the user model and calls `user.update({ stripe_id: customer.id })`.
|
|
131
|
+
`createOrGetStripeUser()` (what the Stripe driver uses) checks `user.stripe_id` first. If the user has one, it retrieves the customer from Stripe. If the customer was deleted (404 or `deleted: true`), it creates a new one. On creation, it auto-fills `name` and `email` from the user model and calls `user.update({ stripe_id: customer.id })`. Pass Stripe customer create params through `manageCustomer.createOrGetStripeUser(user, params)`.
|
|
128
132
|
|
|
129
133
|
Additional methods on `manageCustomer`:
|
|
130
134
|
- `stripeId(user)` -- returns `user.stripe_id`
|
|
@@ -137,19 +141,20 @@ Additional methods on `manageCustomer`:
|
|
|
137
141
|
### Payment Methods
|
|
138
142
|
|
|
139
143
|
```typescript
|
|
140
|
-
//
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
// Set as default (by Stripe payment method ID string)
|
|
144
|
-
const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx')
|
|
144
|
+
// The provider's stored methods, default marked: { id, type, brand, last4, expMonth, expYear, isDefault }
|
|
145
|
+
const methods = await Payment.paymentMethods(user)
|
|
145
146
|
|
|
146
|
-
//
|
|
147
|
-
|
|
147
|
+
// Detach one, by the provider's id
|
|
148
|
+
await Payment.removePaymentMethod(user, 'pm_xxx')
|
|
148
149
|
|
|
149
|
-
//
|
|
150
|
+
// Stripe only
|
|
151
|
+
const pm = await Payment.addPaymentMethod(user, 'pm_xxx')
|
|
152
|
+
const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx')
|
|
150
153
|
const intent = await Payment.createSetupIntent(user, { payment_method_types: ['card'] })
|
|
151
154
|
```
|
|
152
155
|
|
|
156
|
+
`removePaymentMethod()` used to take the local `payment_methods` row id as well; `managePaymentMethod.deletePaymentMethod(user, id)` still does.
|
|
157
|
+
|
|
153
158
|
`addPaymentMethod()` accepts a string (Stripe PM ID) or Stripe.PaymentMethod object. It attaches the PM to the customer if not already attached, then stores it in the `payment_methods` table with `type`, `last_four`, `brand`, `exp_year`, `exp_month`, `user_id`, `provider_id`.
|
|
154
159
|
|
|
155
160
|
`setUserDefaultPayment()` accepts a Stripe PM ID string, clears existing `is_default` flags, sets the new default in the database, and updates `invoice_settings.default_payment_method` on the Stripe customer.
|
|
@@ -549,9 +554,9 @@ The instance methods are exactly the keys of `createBillableMethods` in
|
|
|
549
554
|
- Stripe API keys MUST be in `.env` as `STRIPE_SECRET_KEY` and `STRIPE_PUBLISHABLE_KEY` -- never hardcode them in config files
|
|
550
555
|
- The Stripe client is created lazily, on first use: importing `@stacksjs/payments` without `STRIPE_SECRET_KEY` (or without the opt-in `stripe` package) is fine, and the first Stripe call throws naming what is missing
|
|
551
556
|
- All amounts are in cents -- use `toCents()` and `toDollars()` for conversion
|
|
552
|
-
- `charge()` creates AND confirms the PaymentIntent in one step
|
|
557
|
+
- `Payment.charge()` (Stripe driver) creates AND confirms the PaymentIntent in one step
|
|
553
558
|
- `subscribe()` resolves the price via `lookup_key`, not a direct Stripe price ID
|
|
554
|
-
-
|
|
559
|
+
- `Payment.removePaymentMethod()` and the billable instance method `removePaymentMethod()` both take the provider's id (string); `managePaymentMethod.deletePaymentMethod()` takes the local row id (number)
|
|
555
560
|
- `setDefaultPaymentMethod` has two variants: one takes a Stripe PM ID string (`setUserDefaultPayment`), the other takes a database ID number
|
|
556
561
|
- `getOrCreateCustomer()` handles deleted Stripe customers by recreating them
|
|
557
562
|
- Subscription status checks query the local database, not Stripe directly
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.83",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
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.75.
|
|
5
|
+
"version": "0.75.83",
|
|
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.35",
|
|
58
|
-
"@stacksjs/mobile": "^0.75.
|
|
58
|
+
"@stacksjs/mobile": "^0.75.83",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.378",
|
|
60
60
|
"@xterm/addon-fit": "^0.11.0",
|
|
61
61
|
"@xterm/xterm": "^6.0.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.4.2
|
|
@@ -57,7 +57,7 @@ describe('Native components', () => {
|
|
|
57
57
|
it('names the screen the back button returns to, and gives way to the title', () => {
|
|
58
58
|
const bar = read('NativeNavBar')
|
|
59
59
|
expect(bar).toContain('enterNavTrail(navTrail(), window.location.pathname + window.location.search)')
|
|
60
|
-
expect(bar).toContain('@container (max-width:
|
|
60
|
+
expect(bar).toContain('@container (max-width: 6.5rem)')
|
|
61
61
|
expect(bar).toContain("useReactiveProp<boolean>('titleOnScroll', false)")
|
|
62
62
|
})
|
|
63
63
|
|
|
@@ -171,7 +171,10 @@ function tapBack(event: MouseEvent): void {
|
|
|
171
171
|
.native-nav-back:active { opacity: 0.55; }
|
|
172
172
|
.native-nav-back-icon { flex-shrink: 0; width: 1.6rem; height: 1.6rem; }
|
|
173
173
|
.native-nav-back-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
174
|
-
|
|
174
|
+
/* Where the centred title leaves too little room for a word, the chevron
|
|
175
|
+
alone, as on iOS: at 4.5rem a long title still left "Wo…" for "Workout"
|
|
176
|
+
on a 402pt phone. 6.5rem fits a one-word name beside the chevron. */
|
|
177
|
+
@container (max-width: 6.5rem) {
|
|
175
178
|
.native-nav-back-label { display: none; }
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -26,9 +26,9 @@ const backgroundImage = props.backgroundImage || '/svgs/404.svg'
|
|
|
26
26
|
Sorry, we couldn't find the page you're looking for.
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
<!--
|
|
30
|
-
|
|
31
|
-
<a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-[#
|
|
29
|
+
<!-- The Stacks cobalt, from the logo's plate. White on #1747c9 is
|
|
30
|
+
7.55:1 and on the #0f359d hover 10.41:1, so both states clear AA. -->
|
|
31
|
+
<a href="/" class="inline-block mt-10 px-4 py-2 text-white bg-[#1747c9] hover:bg-[#0f359d] rounded">
|
|
32
32
|
Go back home
|
|
33
33
|
</a>
|
|
34
34
|
</main>
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
framework accent: it sits inches from the artwork, and a blue mark
|
|
25
25
|
against a warm dusk scene reads as a mistake in any app's brand. The
|
|
26
26
|
dark value is the same ramp's coral, from the scene's night variant. -->
|
|
27
|
-
<div class="my-6 h-1 w-14 bg-[#
|
|
27
|
+
<div class="my-6 h-1 w-14 bg-[#1747c9] dark:bg-[#7ea3ff] rounded-full"></div>
|
|
28
28
|
|
|
29
29
|
<h1 class="mb-2 font-semibold text-2xl text-neutral-950 dark:text-white">Page not found</h1>
|
|
30
30
|
|