@stacksjs/defaults 0.75.20 → 0.75.22
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.
|
@@ -292,6 +292,9 @@ if (!result.isErr)
|
|
|
292
292
|
console.log(formatSetupReport(result.value).join('\n'))
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
+
It also creates the coupons in `saas.coupons` with their promotion codes, and
|
|
296
|
+
the customer portal configuration in `saas.portal` (see Customer Portal).
|
|
297
|
+
|
|
295
298
|
This iterates `saas.plans` and reconciles each one against the live account: a
|
|
296
299
|
product is matched by name and reused, and each pricing option is matched by its
|
|
297
300
|
`lookup_key` (from the `key` field). Prices are immutable in Stripe, so a changed
|
|
@@ -303,6 +306,36 @@ Re-running is safe and converges. It does not use `products.search` on purpose:
|
|
|
303
306
|
that index is eventually consistent, so a second run inside the lag window would
|
|
304
307
|
find nothing and create a duplicate.
|
|
305
308
|
|
|
309
|
+
### Customer Portal
|
|
310
|
+
|
|
311
|
+
The Stripe-hosted page where a customer cancels or changes a subscription,
|
|
312
|
+
updates their card and downloads invoices.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Customer id, or a user with a stripe_id. Single-use and short-lived:
|
|
316
|
+
// create it when the customer clicks, then redirect to session.url.
|
|
317
|
+
const session = await Payment.billingPortal('cus_xxx', { returnUrl: 'https://app.example/account' })
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Declare what the portal allows in `config/saas.ts`, and `buddy stripe:setup`
|
|
321
|
+
keeps one configuration in the account to match it:
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
portal: {
|
|
325
|
+
headline: 'Manage your plan',
|
|
326
|
+
returnUrl: 'https://app.example/account', // used when a session names none
|
|
327
|
+
cancel: 'at_period_end', // or 'immediately', or false
|
|
328
|
+
paymentMethodUpdate: true, // default true
|
|
329
|
+
invoiceHistory: true, // default true
|
|
330
|
+
emailUpdate: true, // default true
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The configuration is tagged `metadata: { managed_by: 'stacks', stacks_app: <APP_NAME> }`
|
|
335
|
+
and found by that tag, so several apps can share one Stripe account without
|
|
336
|
+
editing each other's portal. `billingPortal()` uses it, or the account default
|
|
337
|
+
when the app declares no portal.
|
|
338
|
+
|
|
306
339
|
### Utility Functions
|
|
307
340
|
|
|
308
341
|
```typescript
|
|
@@ -413,13 +446,31 @@ Plans use `productName`, `description`, `metadata`, and a `pricing` array where
|
|
|
413
446
|
],
|
|
414
447
|
webhook: { endpoint: 'your-webhook-endpoint', secret: 'your-webhook-secret' },
|
|
415
448
|
currencies: ['usd'],
|
|
416
|
-
coupons: [
|
|
449
|
+
coupons: [
|
|
450
|
+
{
|
|
451
|
+
id: 'six_months_free', // the Stripe coupon id; how a re-run finds it
|
|
452
|
+
name: '6 months free',
|
|
453
|
+
percentOff: 100, // or amountOff (minor units) + currency
|
|
454
|
+
duration: 'repeating', // 'once' | 'repeating' | 'forever'
|
|
455
|
+
durationInMonths: 6,
|
|
456
|
+
appliesTo: ['Stacks Hobby'], // plan productNames; omit for every product
|
|
457
|
+
codes: ['SIXFREE'], // what customers type: Stripe promotion codes
|
|
458
|
+
},
|
|
459
|
+
],
|
|
417
460
|
products: [
|
|
418
461
|
{ name: 'Stacks Hobby', description: '...', images: ['image-url'] },
|
|
419
462
|
],
|
|
420
463
|
} satisfies SaasConfig
|
|
421
464
|
```
|
|
422
465
|
|
|
466
|
+
Coupons are immutable in Stripe. One that exists with a different discount is
|
|
467
|
+
reported as a conflict and left alone (deleting it would void codes already
|
|
468
|
+
handed out), and `stripe:setup` exits non-zero so CI notices. Change a
|
|
469
|
+
discount by giving it a new `id`. Stripe restricts a coupon by product, not
|
|
470
|
+
price, so a plan that must not get a discount needs a product of its own - or
|
|
471
|
+
apply the promotion code yourself at checkout (`discounts: [{ promotion_code }]`)
|
|
472
|
+
for the plans it is meant for.
|
|
473
|
+
|
|
423
474
|
## Database Tables Used
|
|
424
475
|
- `subscriptions` -- columns: `user_id`, `type`, `unit_price`, `provider_id`, `provider_status`, `provider_price_id`, `quantity`, `trial_ends_at`, `ends_at`, `provider_type`, `last_used_at`
|
|
425
476
|
- `payment_methods` -- columns: `id`, `type`, `last_four`, `brand`, `exp_year`, `exp_month`, `user_id`, `provider_id`, `is_default`
|
package/ide/vscode/package.json
CHANGED
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.22",
|
|
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.75.
|
|
58
|
+
"@stacksjs/mobile": "^0.75.22",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|