@shware/purchase 7.5.0 → 8.0.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.
- package/README.md +25 -14
- package/dist/app-store/config.cjs +24 -3
- package/dist/app-store/config.cjs.map +1 -1
- package/dist/app-store/config.d.cts +14 -2
- package/dist/app-store/config.d.cts.map +1 -1
- package/dist/app-store/config.d.mts +14 -2
- package/dist/app-store/config.d.mts.map +1 -1
- package/dist/app-store/config.mjs +24 -4
- package/dist/app-store/config.mjs.map +1 -1
- package/dist/app-store/index.cjs +2 -0
- package/dist/app-store/index.d.cts +3 -3
- package/dist/app-store/index.d.mts +3 -3
- package/dist/app-store/index.mjs +3 -3
- package/dist/app-store/mapper.cjs +22 -0
- package/dist/app-store/mapper.cjs.map +1 -1
- package/dist/app-store/mapper.d.cts +17 -1
- package/dist/app-store/mapper.d.cts.map +1 -1
- package/dist/app-store/mapper.d.mts +17 -1
- package/dist/app-store/mapper.d.mts.map +1 -1
- package/dist/app-store/mapper.mjs +22 -1
- package/dist/app-store/mapper.mjs.map +1 -1
- package/dist/catalog.cjs +22 -0
- package/dist/catalog.cjs.map +1 -0
- package/dist/catalog.d.cts +21 -0
- package/dist/catalog.d.cts.map +1 -0
- package/dist/catalog.d.mts +21 -0
- package/dist/catalog.d.mts.map +1 -0
- package/dist/catalog.mjs +19 -0
- package/dist/catalog.mjs.map +1 -0
- package/dist/google-play/config.cjs +23 -3
- package/dist/google-play/config.cjs.map +1 -1
- package/dist/google-play/config.d.cts +14 -2
- package/dist/google-play/config.d.cts.map +1 -1
- package/dist/google-play/config.d.mts +14 -2
- package/dist/google-play/config.d.mts.map +1 -1
- package/dist/google-play/config.mjs +23 -4
- package/dist/google-play/config.mjs.map +1 -1
- package/dist/google-play/index.cjs +2 -0
- package/dist/google-play/index.d.cts +3 -3
- package/dist/google-play/index.d.mts +3 -3
- package/dist/google-play/index.mjs +3 -3
- package/dist/google-play/mapper.cjs +12 -0
- package/dist/google-play/mapper.cjs.map +1 -1
- package/dist/google-play/mapper.d.cts +10 -1
- package/dist/google-play/mapper.d.cts.map +1 -1
- package/dist/google-play/mapper.d.mts +10 -1
- package/dist/google-play/mapper.d.mts.map +1 -1
- package/dist/google-play/mapper.mjs +12 -1
- package/dist/google-play/mapper.mjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +4 -4
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/dist/stripe/config.cjs +50 -4
- package/dist/stripe/config.cjs.map +1 -1
- package/dist/stripe/config.d.cts +36 -2
- package/dist/stripe/config.d.cts.map +1 -1
- package/dist/stripe/config.d.mts +36 -2
- package/dist/stripe/config.d.mts.map +1 -1
- package/dist/stripe/config.mjs +50 -5
- package/dist/stripe/config.mjs.map +1 -1
- package/dist/stripe/index.cjs +5 -0
- package/dist/stripe/index.d.cts +3 -2
- package/dist/stripe/index.d.mts +3 -2
- package/dist/stripe/index.mjs +3 -2
- package/dist/stripe/plan-change.cjs +400 -0
- package/dist/stripe/plan-change.cjs.map +1 -0
- package/dist/stripe/plan-change.d.cts +142 -0
- package/dist/stripe/plan-change.d.cts.map +1 -0
- package/dist/stripe/plan-change.d.mts +142 -0
- package/dist/stripe/plan-change.d.mts.map +1 -0
- package/dist/stripe/plan-change.mjs +397 -0
- package/dist/stripe/plan-change.mjs.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,19 +63,24 @@ const entitled = (AVAILABLE_STATUSES as readonly SubscriptionStatus[]).includes(
|
|
|
63
63
|
|
|
64
64
|
## Product catalogs
|
|
65
65
|
|
|
66
|
-
Each config is built with a fluent, type-checked chain.
|
|
67
|
-
namespace
|
|
68
|
-
|
|
66
|
+
Each config is built with a fluent, type-checked chain. Every config takes the product id
|
|
67
|
+
`namespace`, your `plans` (lowest tier first) and your `billingPeriods` (shortest first), and infers
|
|
68
|
+
its types from them, so no type arguments are needed; a config with no subscriptions passes empty
|
|
69
|
+
arrays. Product ids must start with the namespace, plans and periods must be declared ones, and
|
|
70
|
+
duplicate products or plan/period pairs fail at compile time and at runtime. Credit metadata
|
|
69
71
|
is `{ credits: number; expiresIn: string }` where `expiresIn` is an
|
|
70
72
|
[`ms`](https://github.com/vercel/ms) duration.
|
|
71
73
|
|
|
72
74
|
```ts
|
|
73
|
-
import {
|
|
75
|
+
import { defineAppStoreConfig, defineGooglePlayConfig, defineStripeConfig } from '@shware/purchase';
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const PLANS = ['STARTER', 'PRO'] as const;
|
|
78
|
+
const BILLING_PERIODS = ['MONTHLY', 'YEARLY'] as const;
|
|
77
79
|
|
|
78
|
-
export const stripe =
|
|
80
|
+
export const stripe = defineStripeConfig({
|
|
81
|
+
namespace: 'com.example.stripe',
|
|
82
|
+
plans: PLANS,
|
|
83
|
+
billingPeriods: BILLING_PERIODS,
|
|
79
84
|
mode: env.STRIPE_SECRET_KEY.startsWith('sk_test_') ? 'test' : 'live',
|
|
80
85
|
allowPromotionCodes: false,
|
|
81
86
|
cancellationCouponId: 'retention_30pct_3mo',
|
|
@@ -93,7 +98,10 @@ export const stripe = StripeConfig.create<'com.example.stripe', Plan, BillingPer
|
|
|
93
98
|
.price('price_live_starter_v1', { credits: 100, expiresIn: '30 days' }) // grandfathered
|
|
94
99
|
.default('price_live_starter_v2', { test: 'price_test_starter' });
|
|
95
100
|
|
|
96
|
-
export const appStore =
|
|
101
|
+
export const appStore = defineAppStoreConfig({
|
|
102
|
+
namespace: 'com.example.appstore',
|
|
103
|
+
plans: PLANS,
|
|
104
|
+
billingPeriods: BILLING_PERIODS,
|
|
97
105
|
appId: '1234567890',
|
|
98
106
|
bundleId: 'com.example.app',
|
|
99
107
|
})
|
|
@@ -102,7 +110,10 @@ export const appStore = AppStoreConfig.create<'com.example.appstore', Plan, Bill
|
|
|
102
110
|
.product('com.example.appstore.sub.monthly.starter.v1', { credits: 100, expiresIn: '30d' })
|
|
103
111
|
.default('com.example.appstore.sub.monthly.starter.v1');
|
|
104
112
|
|
|
105
|
-
export const googlePlay =
|
|
113
|
+
export const googlePlay = defineGooglePlayConfig({
|
|
114
|
+
namespace: 'com.example.googleplay',
|
|
115
|
+
plans: PLANS,
|
|
116
|
+
billingPeriods: BILLING_PERIODS,
|
|
106
117
|
package: 'com.example.app',
|
|
107
118
|
audience: 'api.example.com', // expected audience of the RTDN push OIDC token
|
|
108
119
|
})
|
|
@@ -115,11 +126,11 @@ export const googlePlay = GooglePlayConfig.create<'com.example.googleplay', Plan
|
|
|
115
126
|
|
|
116
127
|
Lookups:
|
|
117
128
|
|
|
118
|
-
| Config | Methods
|
|
119
|
-
| ------------------ |
|
|
120
|
-
| `StripeConfig` | `productIds`, `getPriceId(productId)`, `getPlan`, `getBillingPeriod`, `getMode`, `getCreditAmount(productId, priceId?)`, `getCreditExpiresAt(productId, priceId?)`
|
|
121
|
-
| `AppStoreConfig` | `productIds`, `getDefaultProductId(plan, billingPeriod)`, `getPlan`, `getBillingPeriod`, `getMode`, `getCreditAmount`, `getCreditExpiresAt`
|
|
122
|
-
| `GooglePlayConfig` | `productIds`, `getDefaultPlanIds(productId)`, `getPlan(productId, planId)`, `getBillingPeriod(productId, planId)`, `getMode`, `getCreditAmount(productId, planId?)`, `getCreditExpiresAt(productId, planId?)`
|
|
129
|
+
| Config | Methods |
|
|
130
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `StripeConfig` | `productIds`, `getPriceId(productId)`, `getProductId(priceId)`, `getPlan`, `getBillingPeriod`, `getMode`, `isSubscription`, `comparePlans(from, to)`, `getCreditAmount(productId, priceId?)`, `getCreditExpiresAt(productId, priceId?)` |
|
|
132
|
+
| `AppStoreConfig` | `productIds`, `getDefaultProductId(plan, billingPeriod)`, `getPlan`, `getBillingPeriod`, `getMode`, `getCreditAmount`, `getCreditExpiresAt` |
|
|
133
|
+
| `GooglePlayConfig` | `productIds`, `getDefaultPlanIds(productId)`, `getPlan(productId, planId)`, `getBillingPeriod(productId, planId)`, `getMode`, `getCreditAmount(productId, planId?)`, `getCreditExpiresAt(productId, planId?)` |
|
|
123
134
|
|
|
124
135
|
In Stripe test mode the `test` twin of the default price is what `getPriceId` returns, and it
|
|
125
136
|
resolves to the default price's credits. Non-default prices, products and base plans stay declared
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_catalog = require("../catalog.cjs");
|
|
3
4
|
let _shware_utils = require("@shware/utils");
|
|
4
5
|
let ms = require("ms");
|
|
5
6
|
ms = require_runtime.__toESM(ms, 1);
|
|
6
7
|
//#region src/app-store/config.ts
|
|
7
8
|
const addMethod = Symbol("addMethod");
|
|
9
|
+
/** The private constructor's one door, for this module's `define` function only. */
|
|
10
|
+
const construct = Symbol("construct");
|
|
8
11
|
var Subscription = class {
|
|
9
12
|
plan;
|
|
10
13
|
billingPeriod;
|
|
@@ -27,6 +30,7 @@ var Subscription = class {
|
|
|
27
30
|
credits: 0,
|
|
28
31
|
expiresIn: "0"
|
|
29
32
|
}) => {
|
|
33
|
+
require_catalog.assertInNamespace(this.config.namespace, productId);
|
|
30
34
|
(0, _shware_utils.invariant)(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
31
35
|
this.products.set(productId, metadata);
|
|
32
36
|
return this;
|
|
@@ -41,6 +45,9 @@ var Subscription = class {
|
|
|
41
45
|
var AppStoreConfig = class AppStoreConfig {
|
|
42
46
|
appId;
|
|
43
47
|
bundleId;
|
|
48
|
+
namespace;
|
|
49
|
+
plans;
|
|
50
|
+
billingPeriods;
|
|
44
51
|
products;
|
|
45
52
|
consumables;
|
|
46
53
|
subscriptions;
|
|
@@ -59,6 +66,10 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
59
66
|
return this;
|
|
60
67
|
};
|
|
61
68
|
constructor(options) {
|
|
69
|
+
require_catalog.assertCatalog(options);
|
|
70
|
+
this.namespace = options.namespace;
|
|
71
|
+
this.plans = options.plans;
|
|
72
|
+
this.billingPeriods = options.billingPeriods;
|
|
62
73
|
this.appId = options.appId;
|
|
63
74
|
this.bundleId = options.bundleId;
|
|
64
75
|
this.products = /* @__PURE__ */ new Map();
|
|
@@ -66,23 +77,24 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
66
77
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
67
78
|
this.nonConsumables = /* @__PURE__ */ new Set();
|
|
68
79
|
}
|
|
69
|
-
static
|
|
70
|
-
return new AppStoreConfig(options);
|
|
71
|
-
};
|
|
80
|
+
static [construct] = (options) => new AppStoreConfig(options);
|
|
72
81
|
/** The same plan may be declared once per billing period; duplicate pairs are rejected. */
|
|
73
82
|
subscription = (plan, billingPeriod) => {
|
|
83
|
+
require_catalog.assertDeclared(this, plan, billingPeriod);
|
|
74
84
|
return new Subscription(this, plan, billingPeriod);
|
|
75
85
|
};
|
|
76
86
|
consumable = (productId, metadata = {
|
|
77
87
|
credits: 0,
|
|
78
88
|
expiresIn: "0"
|
|
79
89
|
}) => {
|
|
90
|
+
require_catalog.assertInNamespace(this.namespace, productId);
|
|
80
91
|
(0, _shware_utils.invariant)(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
81
92
|
this.consumables.add(productId);
|
|
82
93
|
this.products.set(productId, metadata);
|
|
83
94
|
return this;
|
|
84
95
|
};
|
|
85
96
|
nonConsumable = (productId) => {
|
|
97
|
+
require_catalog.assertInNamespace(this.namespace, productId);
|
|
86
98
|
(0, _shware_utils.invariant)(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
87
99
|
this.nonConsumables.add(productId);
|
|
88
100
|
this.products.set(productId, null);
|
|
@@ -124,7 +136,16 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
124
136
|
return new Date(Date.now() + expiresIn).toISOString();
|
|
125
137
|
};
|
|
126
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* Declare the AppStore product catalog. Every type parameter is inferred from the options:
|
|
141
|
+
* the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`
|
|
142
|
+
* (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.
|
|
143
|
+
*/
|
|
144
|
+
function defineAppStoreConfig(options) {
|
|
145
|
+
return AppStoreConfig[construct](options);
|
|
146
|
+
}
|
|
127
147
|
//#endregion
|
|
128
148
|
exports.AppStoreConfig = AppStoreConfig;
|
|
149
|
+
exports.defineAppStoreConfig = defineAppStoreConfig;
|
|
129
150
|
|
|
130
151
|
//# sourceMappingURL=config.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.cjs","names":[],"sources":["../../src/app-store/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport type { Metadata } from '../types';\n\nconst addMethod = Symbol('addMethod');\n\nclass Subscription<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends string = never,\n> {\n readonly plan: PE;\n readonly billingPeriod: BP;\n readonly config: AppStoreConfig<NS, PE, BP, PL, PI>;\n readonly products: Map<string, Metadata>;\n\n defaultProductId: I | null;\n\n constructor(config: AppStoreConfig<NS, PE, BP, PL, PI>, plan: PE, billingPeriod: BP) {\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.config = config;\n this.products = new Map();\n this.defaultProductId = null;\n }\n\n /**\n * Non-default products stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * sell a legacy product to new users unless that is intentional.\n */\n product = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Subscription<NS, PE, BP, PL, PI | K, I | K> => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n return this as Subscription<NS, PE, BP, PL, PI | K, I | K>;\n };\n\n default = (defaultProductId: I) => {\n invariant(\n this.products.has(defaultProductId),\n `Default product ${defaultProductId} is not declared`\n );\n this.defaultProductId = defaultProductId;\n this.config[addMethod](this as never);\n return this.config;\n };\n}\n\ntype Options = { appId: `${number}`; bundleId: string };\n\nexport class AppStoreConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n readonly appId: string;\n readonly bundleId: string;\n\n private readonly products: Map<string, Metadata | null>;\n private readonly consumables: Set<string>;\n private readonly subscriptions: Map<string, Subscription<NS, PE, BP, PL, PI>>;\n private readonly nonConsumables: Set<string>;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (subscription: Subscription<NS, PE, BP, PL, PI>) => {\n const key = `${subscription.plan}:${subscription.billingPeriod}`;\n invariant(!this.subscriptions.has(key), `Duplicate plan ${key}`);\n subscription.products.forEach((metadata, productId) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n });\n this.subscriptions.set(key, subscription);\n return this;\n };\n\n private constructor(options: Options) {\n this.appId = options.appId;\n this.bundleId = options.bundleId;\n this.products = new Map();\n this.consumables = new Set();\n this.subscriptions = new Map();\n this.nonConsumables = new Set();\n }\n\n static create = <NS extends Lowercase<string>, PE extends string, BP extends string = string>(\n options: Options\n ) => {\n return new AppStoreConfig<NS, PE, BP>(options);\n };\n\n /** The same plan may be declared once per billing period; duplicate pairs are rejected. */\n subscription = <K extends PE, P extends BP>(\n plan: K,\n billingPeriod: `${K}:${P}` extends PL ? never : P\n ) => {\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the cast (tsgolint false positive)\n return new Subscription<NS, PE, BP, `${K}:${P}` | PL, PI>(this as never, plan, billingPeriod);\n };\n\n consumable = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.consumables.add(productId);\n this.products.set(productId, metadata);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n nonConsumable = <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.nonConsumables.add(productId);\n this.products.set(productId, null);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n /** Product id that new subscribers should purchase for the plan and billing period. */\n getDefaultProductId = (plan: PE, billingPeriod: BP): string => {\n const subscription = this.subscriptions.get(`${plan}:${billingPeriod}`);\n invariant(subscription, `Subscription not found for ${plan}:${billingPeriod}`);\n invariant(\n subscription.defaultProductId,\n `Default product not found for ${plan}:${billingPeriod}`\n );\n return subscription.defaultProductId;\n };\n\n getPlan = (productId: string): PE => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.plan;\n }\n throw new Error(`Plan not found for product ${productId}`);\n };\n\n getBillingPeriod = (productId: string): BP => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.billingPeriod;\n }\n throw new Error(`Billing period not found for product ${productId}`);\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n if (this.consumables.has(productId) || this.nonConsumables.has(productId)) return 'payment';\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return 'subscription';\n }\n throw new Error(`Mode not found for product ${productId}`);\n };\n\n getCreditAmount = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config !== undefined, `Product not found for ${productId}`);\n invariant(config !== null, `Credits not available for non-consumable product ${productId}`);\n return config.credits;\n };\n\n private readonly getCreditExpiresIn = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config, `Product not found for ${productId}`);\n return ms(config.expiresIn);\n };\n\n getCreditExpiresAt = (productId: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.cjs","names":[],"sources":["../../src/app-store/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport { type Catalog, assertCatalog, assertDeclared, assertInNamespace } from '../catalog';\nimport type { Metadata } from '../types';\n\nconst addMethod = Symbol('addMethod');\n/** The private constructor's one door, for this module's `define` function only. */\nconst construct = Symbol('construct');\n\nclass Subscription<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends string = never,\n> {\n readonly plan: PE;\n readonly billingPeriod: BP;\n readonly config: AppStoreConfig<NS, PE, BP, PL, PI>;\n readonly products: Map<string, Metadata>;\n\n defaultProductId: I | null;\n\n constructor(config: AppStoreConfig<NS, PE, BP, PL, PI>, plan: PE, billingPeriod: BP) {\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.config = config;\n this.products = new Map();\n this.defaultProductId = null;\n }\n\n /**\n * Non-default products stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * sell a legacy product to new users unless that is intentional.\n */\n product = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Subscription<NS, PE, BP, PL, PI | K, I | K> => {\n assertInNamespace(this.config.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n return this as Subscription<NS, PE, BP, PL, PI | K, I | K>;\n };\n\n default = (defaultProductId: I) => {\n invariant(\n this.products.has(defaultProductId),\n `Default product ${defaultProductId} is not declared`\n );\n this.defaultProductId = defaultProductId;\n this.config[addMethod](this as never);\n return this.config;\n };\n}\n\ntype Options<NS extends Lowercase<string>, PE extends string, BP extends string> = Catalog<\n NS,\n PE,\n BP\n> & { appId: `${number}`; bundleId: string };\n\nexport class AppStoreConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n readonly appId: string;\n readonly bundleId: string;\n readonly namespace: NS;\n readonly plans: readonly PE[];\n readonly billingPeriods: readonly BP[];\n\n private readonly products: Map<string, Metadata | null>;\n private readonly consumables: Set<string>;\n private readonly subscriptions: Map<string, Subscription<NS, PE, BP, PL, PI>>;\n private readonly nonConsumables: Set<string>;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (subscription: Subscription<NS, PE, BP, PL, PI>) => {\n const key = `${subscription.plan}:${subscription.billingPeriod}`;\n invariant(!this.subscriptions.has(key), `Duplicate plan ${key}`);\n subscription.products.forEach((metadata, productId) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n });\n this.subscriptions.set(key, subscription);\n return this;\n };\n\n private constructor(options: Options<NS, PE, BP>) {\n assertCatalog(options);\n this.namespace = options.namespace;\n this.plans = options.plans;\n this.billingPeriods = options.billingPeriods;\n this.appId = options.appId;\n this.bundleId = options.bundleId;\n this.products = new Map();\n this.consumables = new Set();\n this.subscriptions = new Map();\n this.nonConsumables = new Set();\n }\n\n static [construct] = <NS extends Lowercase<string>, PE extends string, BP extends string>(\n options: Options<NS, PE, BP>\n ) => new AppStoreConfig<NS, PE, BP>(options);\n\n /** The same plan may be declared once per billing period; duplicate pairs are rejected. */\n subscription = <K extends PE, P extends BP>(\n plan: K,\n billingPeriod: `${K}:${P}` extends PL ? never : P\n ) => {\n assertDeclared(this, plan, billingPeriod);\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the cast (tsgolint false positive)\n return new Subscription<NS, PE, BP, `${K}:${P}` | PL, PI>(this as never, plan, billingPeriod);\n };\n\n consumable = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ) => {\n assertInNamespace(this.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.consumables.add(productId);\n this.products.set(productId, metadata);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n nonConsumable = <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K) => {\n assertInNamespace(this.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.nonConsumables.add(productId);\n this.products.set(productId, null);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n /** Product id that new subscribers should purchase for the plan and billing period. */\n getDefaultProductId = (plan: PE, billingPeriod: BP): string => {\n const subscription = this.subscriptions.get(`${plan}:${billingPeriod}`);\n invariant(subscription, `Subscription not found for ${plan}:${billingPeriod}`);\n invariant(\n subscription.defaultProductId,\n `Default product not found for ${plan}:${billingPeriod}`\n );\n return subscription.defaultProductId;\n };\n\n getPlan = (productId: string): PE => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.plan;\n }\n throw new Error(`Plan not found for product ${productId}`);\n };\n\n getBillingPeriod = (productId: string): BP => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.billingPeriod;\n }\n throw new Error(`Billing period not found for product ${productId}`);\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n if (this.consumables.has(productId) || this.nonConsumables.has(productId)) return 'payment';\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return 'subscription';\n }\n throw new Error(`Mode not found for product ${productId}`);\n };\n\n getCreditAmount = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config !== undefined, `Product not found for ${productId}`);\n invariant(config !== null, `Credits not available for non-consumable product ${productId}`);\n return config.credits;\n };\n\n private readonly getCreditExpiresIn = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config, `Product not found for ${productId}`);\n return ms(config.expiresIn);\n };\n\n getCreditExpiresAt = (productId: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n\n/**\n * Declare the AppStore product catalog. Every type parameter is inferred from the options:\n * the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`\n * (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.\n */\nexport function defineAppStoreConfig<\n const NS extends Lowercase<string>,\n const PE extends string,\n const BP extends string,\n>(options: Options<NS, PE, BP>): AppStoreConfig<NS, PE, BP> {\n return AppStoreConfig[construct](options);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Catalog } from "../catalog.cjs";
|
|
1
2
|
import { Metadata } from "../types.cjs";
|
|
2
3
|
//#region src/app-store/config.d.ts
|
|
3
4
|
declare const addMethod: unique symbol;
|
|
5
|
+
/** The private constructor's one door, for this module's `define` function only. */
|
|
6
|
+
declare const construct: unique symbol;
|
|
4
7
|
declare class Subscription<NS extends Lowercase<string> = Lowercase<string>, PE extends string = string, BP extends string = string, PL extends string = never, PI extends string = never, I extends string = never> {
|
|
5
8
|
readonly plan: PE;
|
|
6
9
|
readonly billingPeriod: BP;
|
|
@@ -16,13 +19,16 @@ declare class Subscription<NS extends Lowercase<string> = Lowercase<string>, PE
|
|
|
16
19
|
product: <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K, metadata?: Metadata) => Subscription<NS, PE, BP, PL, PI | K, I | K>;
|
|
17
20
|
default: (defaultProductId: I) => AppStoreConfig<NS, PE, BP, PL, PI>;
|
|
18
21
|
}
|
|
19
|
-
type Options = {
|
|
22
|
+
type Options<NS extends Lowercase<string>, PE extends string, BP extends string> = Catalog<NS, PE, BP> & {
|
|
20
23
|
appId: `${number}`;
|
|
21
24
|
bundleId: string;
|
|
22
25
|
};
|
|
23
26
|
export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<string>, PE extends string = string, BP extends string = string, PL extends string = never, PI extends string = never> {
|
|
24
27
|
readonly appId: string;
|
|
25
28
|
readonly bundleId: string;
|
|
29
|
+
readonly namespace: NS;
|
|
30
|
+
readonly plans: readonly PE[];
|
|
31
|
+
readonly billingPeriods: readonly BP[];
|
|
26
32
|
private readonly products;
|
|
27
33
|
private readonly consumables;
|
|
28
34
|
private readonly subscriptions;
|
|
@@ -30,7 +36,7 @@ export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<str
|
|
|
30
36
|
get productIds(): string[];
|
|
31
37
|
[addMethod]: (subscription: Subscription<NS, PE, BP, PL, PI>) => this;
|
|
32
38
|
private constructor();
|
|
33
|
-
static
|
|
39
|
+
static [construct]: <NS_1 extends Lowercase<string>, PE_1 extends string, BP_1 extends string>(options: Options<NS_1, PE_1, BP_1>) => AppStoreConfig<NS_1, PE_1, BP_1, never, never>;
|
|
34
40
|
/** The same plan may be declared once per billing period; duplicate pairs are rejected. */
|
|
35
41
|
subscription: <K extends PE, P extends BP>(plan: K, billingPeriod: `${K}:${P}` extends PL ? never : P) => Subscription<NS, PE, BP, PL | `${K}:${P}`, PI, never>;
|
|
36
42
|
consumable: <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K, metadata?: Metadata) => AppStoreConfig<NS, PE, BP, PL, PI | K>;
|
|
@@ -44,6 +50,12 @@ export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<str
|
|
|
44
50
|
private readonly getCreditExpiresIn;
|
|
45
51
|
getCreditExpiresAt: (productId: string) => string;
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Declare the AppStore product catalog. Every type parameter is inferred from the options:
|
|
55
|
+
* the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`
|
|
56
|
+
* (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.
|
|
57
|
+
*/
|
|
58
|
+
export declare function defineAppStoreConfig<const NS extends Lowercase<string>, const PE extends string, const BP extends string>(options: Options<NS, PE, BP>): AppStoreConfig<NS, PE, BP>;
|
|
47
59
|
//#endregion
|
|
48
60
|
export {};
|
|
49
61
|
//# sourceMappingURL=config.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.cts","names":[],"sources":["../../src/app-store/config.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"config.d.cts","names":[],"sources":["../../src/app-store/config.ts"],"mappings":";;;cAKM;;cAEA;cAEA,aACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA;WAES,MAAM;WACN,eAAe;WACf,QAAQ,eAAe,IAAI,IAAI,IAAI,IAAI;WACvC,UAAU,YAAY;EAE/B,kBAAkB;EAElB,YAAY,QAAQ,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK,MAAM,IAAI,eAAe;;;;;;EAajF,UAAW,aAAa,MAAM,qBAAmB,WACpC,UAAU,aAAa,GAAC,WACzB,aACT,aAAa,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,IAAI;EAO5C,UAAO,kBAAsB,MAAC,eAAA,IAAA,IAAA,IAAA,IAAA;;KAW3B,QAAQ,WAAW,mBAAmB,mBAAmB,qBAAqB,QACjF,IACA,IACA;EACI;EAAoB;;qBAEb,eACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;WAES;WACA;WACA,WAAW;WACX,gBAAgB;WAChB,yBAAyB;mBAEjB;mBACA;mBACA;mBACA;MAEb;GAMH,aAAU,cAAkB,aAAa,IAAI,IAAI,IAAI,IAAI;UAWnD;UAaC,aAAc,aAAW,mBAAmB,qBAAmB,qBAAiB,SAC7E,QAAQ,MAAI,MAAI,UAAG,eAAA,MAAA,MAAA;;EAI9B,eAAgB,UAAU,IAAI,UAAU,IAAE,MAClC,GAAC,kBACW,KAAK,YAAY,aAAa,MAAC,aAAA,IAAA,IAAA,IAAA,QAAA,KAAA,KAAA;EAOnD,aAAc,aAAa,MAAM,qBAAmB,WACvC,UAAU,aAAa,GAAC,WACzB,aAMK,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK;EAGrD,gBAAiB,aAAa,MAAM,qBAAmB,WAAa,UAAU,aAAa,MAK1E,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK;;EAIrD,sBAAmB,MAAU,IAAE,eAAiB;EAUhD,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;EAQP,kBAAe;mBAOE;EAMjB,qBAAkB;;;;;;;wBAWJ,2BACR,WAAW,yBACX,yBACA,mBACN,SAAS,QAAQ,IAAI,IAAI,MAAM,eAAe,IAAI,IAAI"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Catalog } from "../catalog.mjs";
|
|
1
2
|
import { Metadata } from "../types.mjs";
|
|
2
3
|
//#region src/app-store/config.d.ts
|
|
3
4
|
declare const addMethod: unique symbol;
|
|
5
|
+
/** The private constructor's one door, for this module's `define` function only. */
|
|
6
|
+
declare const construct: unique symbol;
|
|
4
7
|
declare class Subscription<NS extends Lowercase<string> = Lowercase<string>, PE extends string = string, BP extends string = string, PL extends string = never, PI extends string = never, I extends string = never> {
|
|
5
8
|
readonly plan: PE;
|
|
6
9
|
readonly billingPeriod: BP;
|
|
@@ -16,13 +19,16 @@ declare class Subscription<NS extends Lowercase<string> = Lowercase<string>, PE
|
|
|
16
19
|
product: <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K, metadata?: Metadata) => Subscription<NS, PE, BP, PL, PI | K, I | K>;
|
|
17
20
|
default: (defaultProductId: I) => AppStoreConfig<NS, PE, BP, PL, PI>;
|
|
18
21
|
}
|
|
19
|
-
type Options = {
|
|
22
|
+
type Options<NS extends Lowercase<string>, PE extends string, BP extends string> = Catalog<NS, PE, BP> & {
|
|
20
23
|
appId: `${number}`;
|
|
21
24
|
bundleId: string;
|
|
22
25
|
};
|
|
23
26
|
export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<string>, PE extends string = string, BP extends string = string, PL extends string = never, PI extends string = never> {
|
|
24
27
|
readonly appId: string;
|
|
25
28
|
readonly bundleId: string;
|
|
29
|
+
readonly namespace: NS;
|
|
30
|
+
readonly plans: readonly PE[];
|
|
31
|
+
readonly billingPeriods: readonly BP[];
|
|
26
32
|
private readonly products;
|
|
27
33
|
private readonly consumables;
|
|
28
34
|
private readonly subscriptions;
|
|
@@ -30,7 +36,7 @@ export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<str
|
|
|
30
36
|
get productIds(): string[];
|
|
31
37
|
[addMethod]: (subscription: Subscription<NS, PE, BP, PL, PI>) => this;
|
|
32
38
|
private constructor();
|
|
33
|
-
static
|
|
39
|
+
static [construct]: <NS_1 extends Lowercase<string>, PE_1 extends string, BP_1 extends string>(options: Options<NS_1, PE_1, BP_1>) => AppStoreConfig<NS_1, PE_1, BP_1, never, never>;
|
|
34
40
|
/** The same plan may be declared once per billing period; duplicate pairs are rejected. */
|
|
35
41
|
subscription: <K extends PE, P extends BP>(plan: K, billingPeriod: `${K}:${P}` extends PL ? never : P) => Subscription<NS, PE, BP, PL | `${K}:${P}`, PI, never>;
|
|
36
42
|
consumable: <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K, metadata?: Metadata) => AppStoreConfig<NS, PE, BP, PL, PI | K>;
|
|
@@ -44,6 +50,12 @@ export declare class AppStoreConfig<NS extends Lowercase<string> = Lowercase<str
|
|
|
44
50
|
private readonly getCreditExpiresIn;
|
|
45
51
|
getCreditExpiresAt: (productId: string) => string;
|
|
46
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Declare the AppStore product catalog. Every type parameter is inferred from the options:
|
|
55
|
+
* the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`
|
|
56
|
+
* (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.
|
|
57
|
+
*/
|
|
58
|
+
export declare function defineAppStoreConfig<const NS extends Lowercase<string>, const PE extends string, const BP extends string>(options: Options<NS, PE, BP>): AppStoreConfig<NS, PE, BP>;
|
|
47
59
|
//#endregion
|
|
48
60
|
export {};
|
|
49
61
|
//# sourceMappingURL=config.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/app-store/config.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../../src/app-store/config.ts"],"mappings":";;;cAKM;;cAEA;cAEA,aACJ,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA,2BACA;WAES,MAAM;WACN,eAAe;WACf,QAAQ,eAAe,IAAI,IAAI,IAAI,IAAI;WACvC,UAAU,YAAY;EAE/B,kBAAkB;EAElB,YAAY,QAAQ,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK,MAAM,IAAI,eAAe;;;;;;EAajF,UAAW,aAAa,MAAM,qBAAmB,WACpC,UAAU,aAAa,GAAC,WACzB,aACT,aAAa,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,IAAI;EAO5C,UAAO,kBAAsB,MAAC,eAAA,IAAA,IAAA,IAAA,IAAA;;KAW3B,QAAQ,WAAW,mBAAmB,mBAAmB,qBAAqB,QACjF,IACA,IACA;EACI;EAAoB;;qBAEb,eACX,WAAW,oBAAoB,mBAC/B,4BACA,4BACA,2BACA;WAES;WACA;WACA,WAAW;WACX,gBAAgB;WAChB,yBAAyB;mBAEjB;mBACA;mBACA;mBACA;MAEb;GAMH,aAAU,cAAkB,aAAa,IAAI,IAAI,IAAI,IAAI;UAWnD;UAaC,aAAc,aAAW,mBAAmB,qBAAmB,qBAAiB,SAC7E,QAAQ,MAAI,MAAI,UAAG,eAAA,MAAA,MAAA;;EAI9B,eAAgB,UAAU,IAAI,UAAU,IAAE,MAClC,GAAC,kBACW,KAAK,YAAY,aAAa,MAAC,aAAA,IAAA,IAAA,IAAA,QAAA,KAAA,KAAA;EAOnD,aAAc,aAAa,MAAM,qBAAmB,WACvC,UAAU,aAAa,GAAC,WACzB,aAMK,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK;EAGrD,gBAAiB,aAAa,MAAM,qBAAmB,WAAa,UAAU,aAAa,MAK1E,eAAe,IAAI,IAAI,IAAI,IAAI,KAAK;;EAIrD,sBAAmB,MAAU,IAAE,eAAiB;EAUhD,UAAO,sBAAwB;EAO/B,mBAAgB,sBAAwB;EAOxC,UAAO;EAQP,kBAAe;mBAOE;EAMjB,qBAAkB;;;;;;;wBAWJ,2BACR,WAAW,yBACX,yBACA,mBACN,SAAS,QAAQ,IAAI,IAAI,MAAM,eAAe,IAAI,IAAI"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { assertCatalog, assertDeclared, assertInNamespace } from "../catalog.mjs";
|
|
1
2
|
import { invariant } from "@shware/utils";
|
|
2
3
|
import ms from "ms";
|
|
3
4
|
//#region src/app-store/config.ts
|
|
4
5
|
const addMethod = Symbol("addMethod");
|
|
6
|
+
/** The private constructor's one door, for this module's `define` function only. */
|
|
7
|
+
const construct = Symbol("construct");
|
|
5
8
|
var Subscription = class {
|
|
6
9
|
plan;
|
|
7
10
|
billingPeriod;
|
|
@@ -24,6 +27,7 @@ var Subscription = class {
|
|
|
24
27
|
credits: 0,
|
|
25
28
|
expiresIn: "0"
|
|
26
29
|
}) => {
|
|
30
|
+
assertInNamespace(this.config.namespace, productId);
|
|
27
31
|
invariant(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
28
32
|
this.products.set(productId, metadata);
|
|
29
33
|
return this;
|
|
@@ -38,6 +42,9 @@ var Subscription = class {
|
|
|
38
42
|
var AppStoreConfig = class AppStoreConfig {
|
|
39
43
|
appId;
|
|
40
44
|
bundleId;
|
|
45
|
+
namespace;
|
|
46
|
+
plans;
|
|
47
|
+
billingPeriods;
|
|
41
48
|
products;
|
|
42
49
|
consumables;
|
|
43
50
|
subscriptions;
|
|
@@ -56,6 +63,10 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
56
63
|
return this;
|
|
57
64
|
};
|
|
58
65
|
constructor(options) {
|
|
66
|
+
assertCatalog(options);
|
|
67
|
+
this.namespace = options.namespace;
|
|
68
|
+
this.plans = options.plans;
|
|
69
|
+
this.billingPeriods = options.billingPeriods;
|
|
59
70
|
this.appId = options.appId;
|
|
60
71
|
this.bundleId = options.bundleId;
|
|
61
72
|
this.products = /* @__PURE__ */ new Map();
|
|
@@ -63,23 +74,24 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
63
74
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
64
75
|
this.nonConsumables = /* @__PURE__ */ new Set();
|
|
65
76
|
}
|
|
66
|
-
static
|
|
67
|
-
return new AppStoreConfig(options);
|
|
68
|
-
};
|
|
77
|
+
static [construct] = (options) => new AppStoreConfig(options);
|
|
69
78
|
/** The same plan may be declared once per billing period; duplicate pairs are rejected. */
|
|
70
79
|
subscription = (plan, billingPeriod) => {
|
|
80
|
+
assertDeclared(this, plan, billingPeriod);
|
|
71
81
|
return new Subscription(this, plan, billingPeriod);
|
|
72
82
|
};
|
|
73
83
|
consumable = (productId, metadata = {
|
|
74
84
|
credits: 0,
|
|
75
85
|
expiresIn: "0"
|
|
76
86
|
}) => {
|
|
87
|
+
assertInNamespace(this.namespace, productId);
|
|
77
88
|
invariant(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
78
89
|
this.consumables.add(productId);
|
|
79
90
|
this.products.set(productId, metadata);
|
|
80
91
|
return this;
|
|
81
92
|
};
|
|
82
93
|
nonConsumable = (productId) => {
|
|
94
|
+
assertInNamespace(this.namespace, productId);
|
|
83
95
|
invariant(!this.products.has(productId), `Duplicate product ${productId}`);
|
|
84
96
|
this.nonConsumables.add(productId);
|
|
85
97
|
this.products.set(productId, null);
|
|
@@ -121,7 +133,15 @@ var AppStoreConfig = class AppStoreConfig {
|
|
|
121
133
|
return new Date(Date.now() + expiresIn).toISOString();
|
|
122
134
|
};
|
|
123
135
|
};
|
|
136
|
+
/**
|
|
137
|
+
* Declare the AppStore product catalog. Every type parameter is inferred from the options:
|
|
138
|
+
* the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`
|
|
139
|
+
* (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.
|
|
140
|
+
*/
|
|
141
|
+
function defineAppStoreConfig(options) {
|
|
142
|
+
return AppStoreConfig[construct](options);
|
|
143
|
+
}
|
|
124
144
|
//#endregion
|
|
125
|
-
export { AppStoreConfig };
|
|
145
|
+
export { AppStoreConfig, defineAppStoreConfig };
|
|
126
146
|
|
|
127
147
|
//# sourceMappingURL=config.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/app-store/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport type { Metadata } from '../types';\n\nconst addMethod = Symbol('addMethod');\n\nclass Subscription<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends string = never,\n> {\n readonly plan: PE;\n readonly billingPeriod: BP;\n readonly config: AppStoreConfig<NS, PE, BP, PL, PI>;\n readonly products: Map<string, Metadata>;\n\n defaultProductId: I | null;\n\n constructor(config: AppStoreConfig<NS, PE, BP, PL, PI>, plan: PE, billingPeriod: BP) {\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.config = config;\n this.products = new Map();\n this.defaultProductId = null;\n }\n\n /**\n * Non-default products stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * sell a legacy product to new users unless that is intentional.\n */\n product = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Subscription<NS, PE, BP, PL, PI | K, I | K> => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n return this as Subscription<NS, PE, BP, PL, PI | K, I | K>;\n };\n\n default = (defaultProductId: I) => {\n invariant(\n this.products.has(defaultProductId),\n `Default product ${defaultProductId} is not declared`\n );\n this.defaultProductId = defaultProductId;\n this.config[addMethod](this as never);\n return this.config;\n };\n}\n\ntype Options = { appId: `${number}`; bundleId: string };\n\nexport class AppStoreConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n readonly appId: string;\n readonly bundleId: string;\n\n private readonly products: Map<string, Metadata | null>;\n private readonly consumables: Set<string>;\n private readonly subscriptions: Map<string, Subscription<NS, PE, BP, PL, PI>>;\n private readonly nonConsumables: Set<string>;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (subscription: Subscription<NS, PE, BP, PL, PI>) => {\n const key = `${subscription.plan}:${subscription.billingPeriod}`;\n invariant(!this.subscriptions.has(key), `Duplicate plan ${key}`);\n subscription.products.forEach((metadata, productId) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n });\n this.subscriptions.set(key, subscription);\n return this;\n };\n\n private constructor(options: Options) {\n this.appId = options.appId;\n this.bundleId = options.bundleId;\n this.products = new Map();\n this.consumables = new Set();\n this.subscriptions = new Map();\n this.nonConsumables = new Set();\n }\n\n static create = <NS extends Lowercase<string>, PE extends string, BP extends string = string>(\n options: Options\n ) => {\n return new AppStoreConfig<NS, PE, BP>(options);\n };\n\n /** The same plan may be declared once per billing period; duplicate pairs are rejected. */\n subscription = <K extends PE, P extends BP>(\n plan: K,\n billingPeriod: `${K}:${P}` extends PL ? never : P\n ) => {\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the cast (tsgolint false positive)\n return new Subscription<NS, PE, BP, `${K}:${P}` | PL, PI>(this as never, plan, billingPeriod);\n };\n\n consumable = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.consumables.add(productId);\n this.products.set(productId, metadata);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n nonConsumable = <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.nonConsumables.add(productId);\n this.products.set(productId, null);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n /** Product id that new subscribers should purchase for the plan and billing period. */\n getDefaultProductId = (plan: PE, billingPeriod: BP): string => {\n const subscription = this.subscriptions.get(`${plan}:${billingPeriod}`);\n invariant(subscription, `Subscription not found for ${plan}:${billingPeriod}`);\n invariant(\n subscription.defaultProductId,\n `Default product not found for ${plan}:${billingPeriod}`\n );\n return subscription.defaultProductId;\n };\n\n getPlan = (productId: string): PE => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.plan;\n }\n throw new Error(`Plan not found for product ${productId}`);\n };\n\n getBillingPeriod = (productId: string): BP => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.billingPeriod;\n }\n throw new Error(`Billing period not found for product ${productId}`);\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n if (this.consumables.has(productId) || this.nonConsumables.has(productId)) return 'payment';\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return 'subscription';\n }\n throw new Error(`Mode not found for product ${productId}`);\n };\n\n getCreditAmount = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config !== undefined, `Product not found for ${productId}`);\n invariant(config !== null, `Credits not available for non-consumable product ${productId}`);\n return config.credits;\n };\n\n private readonly getCreditExpiresIn = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config, `Product not found for ${productId}`);\n return ms(config.expiresIn);\n };\n\n getCreditExpiresAt = (productId: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../../src/app-store/config.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport ms from 'ms';\nimport { type Catalog, assertCatalog, assertDeclared, assertInNamespace } from '../catalog';\nimport type { Metadata } from '../types';\n\nconst addMethod = Symbol('addMethod');\n/** The private constructor's one door, for this module's `define` function only. */\nconst construct = Symbol('construct');\n\nclass Subscription<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n I extends string = never,\n> {\n readonly plan: PE;\n readonly billingPeriod: BP;\n readonly config: AppStoreConfig<NS, PE, BP, PL, PI>;\n readonly products: Map<string, Metadata>;\n\n defaultProductId: I | null;\n\n constructor(config: AppStoreConfig<NS, PE, BP, PL, PI>, plan: PE, billingPeriod: BP) {\n this.plan = plan;\n this.billingPeriod = billingPeriod;\n this.config = config;\n this.products = new Map();\n this.defaultProductId = null;\n }\n\n /**\n * Non-default products stay declared on purpose: they keep resolving for\n * grandfathered subscribers and serve as price-experiment arms. Never\n * sell a legacy product to new users unless that is intentional.\n */\n product = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ): Subscription<NS, PE, BP, PL, PI | K, I | K> => {\n assertInNamespace(this.config.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n return this as Subscription<NS, PE, BP, PL, PI | K, I | K>;\n };\n\n default = (defaultProductId: I) => {\n invariant(\n this.products.has(defaultProductId),\n `Default product ${defaultProductId} is not declared`\n );\n this.defaultProductId = defaultProductId;\n this.config[addMethod](this as never);\n return this.config;\n };\n}\n\ntype Options<NS extends Lowercase<string>, PE extends string, BP extends string> = Catalog<\n NS,\n PE,\n BP\n> & { appId: `${number}`; bundleId: string };\n\nexport class AppStoreConfig<\n NS extends Lowercase<string> = Lowercase<string>,\n PE extends string = string,\n BP extends string = string,\n PL extends string = never,\n PI extends string = never,\n> {\n readonly appId: string;\n readonly bundleId: string;\n readonly namespace: NS;\n readonly plans: readonly PE[];\n readonly billingPeriods: readonly BP[];\n\n private readonly products: Map<string, Metadata | null>;\n private readonly consumables: Set<string>;\n private readonly subscriptions: Map<string, Subscription<NS, PE, BP, PL, PI>>;\n private readonly nonConsumables: Set<string>;\n\n get productIds(): string[] {\n return Array.from(this.products.keys());\n }\n\n // runtime mirror of the type-level guards: generic accumulation only\n // protects a single fluent chain, not statement-style re-entry or casts\n [addMethod] = (subscription: Subscription<NS, PE, BP, PL, PI>) => {\n const key = `${subscription.plan}:${subscription.billingPeriod}`;\n invariant(!this.subscriptions.has(key), `Duplicate plan ${key}`);\n subscription.products.forEach((metadata, productId) => {\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.products.set(productId, metadata);\n });\n this.subscriptions.set(key, subscription);\n return this;\n };\n\n private constructor(options: Options<NS, PE, BP>) {\n assertCatalog(options);\n this.namespace = options.namespace;\n this.plans = options.plans;\n this.billingPeriods = options.billingPeriods;\n this.appId = options.appId;\n this.bundleId = options.bundleId;\n this.products = new Map();\n this.consumables = new Set();\n this.subscriptions = new Map();\n this.nonConsumables = new Set();\n }\n\n static [construct] = <NS extends Lowercase<string>, PE extends string, BP extends string>(\n options: Options<NS, PE, BP>\n ) => new AppStoreConfig<NS, PE, BP>(options);\n\n /** The same plan may be declared once per billing period; duplicate pairs are rejected. */\n subscription = <K extends PE, P extends BP>(\n plan: K,\n billingPeriod: `${K}:${P}` extends PL ? never : P\n ) => {\n assertDeclared(this, plan, billingPeriod);\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the cast (tsgolint false positive)\n return new Subscription<NS, PE, BP, `${K}:${P}` | PL, PI>(this as never, plan, billingPeriod);\n };\n\n consumable = <K extends `${NS}.${Lowercase<string>}`>(\n productId: K extends PI ? never : K,\n metadata: Metadata = { credits: 0, expiresIn: '0' }\n ) => {\n assertInNamespace(this.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.consumables.add(productId);\n this.products.set(productId, metadata);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n nonConsumable = <K extends `${NS}.${Lowercase<string>}`>(productId: K extends PI ? never : K) => {\n assertInNamespace(this.namespace, productId);\n invariant(!this.products.has(productId), `Duplicate product ${productId}`);\n this.nonConsumables.add(productId);\n this.products.set(productId, null);\n return this as AppStoreConfig<NS, PE, BP, PL, PI | K>;\n };\n\n /** Product id that new subscribers should purchase for the plan and billing period. */\n getDefaultProductId = (plan: PE, billingPeriod: BP): string => {\n const subscription = this.subscriptions.get(`${plan}:${billingPeriod}`);\n invariant(subscription, `Subscription not found for ${plan}:${billingPeriod}`);\n invariant(\n subscription.defaultProductId,\n `Default product not found for ${plan}:${billingPeriod}`\n );\n return subscription.defaultProductId;\n };\n\n getPlan = (productId: string): PE => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.plan;\n }\n throw new Error(`Plan not found for product ${productId}`);\n };\n\n getBillingPeriod = (productId: string): BP => {\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return subscription.billingPeriod;\n }\n throw new Error(`Billing period not found for product ${productId}`);\n };\n\n getMode = (productId: string): 'payment' | 'subscription' => {\n if (this.consumables.has(productId) || this.nonConsumables.has(productId)) return 'payment';\n for (const subscription of this.subscriptions.values()) {\n if (subscription.products.has(productId)) return 'subscription';\n }\n throw new Error(`Mode not found for product ${productId}`);\n };\n\n getCreditAmount = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config !== undefined, `Product not found for ${productId}`);\n invariant(config !== null, `Credits not available for non-consumable product ${productId}`);\n return config.credits;\n };\n\n private readonly getCreditExpiresIn = (productId: string): number => {\n const config = this.products.get(productId);\n invariant(config, `Product not found for ${productId}`);\n return ms(config.expiresIn);\n };\n\n getCreditExpiresAt = (productId: string): string => {\n const expiresIn = this.getCreditExpiresIn(productId);\n return new Date(Date.now() + expiresIn).toISOString();\n };\n}\n\n/**\n * Declare the AppStore product catalog. Every type parameter is inferred from the options:\n * the namespace from `namespace`, the plans and billing periods from `plans` and `billingPeriods`\n * (`const`, so a literal array needs no `as const`). Products are then added with the fluent chain.\n */\nexport function defineAppStoreConfig<\n const NS extends Lowercase<string>,\n const PE extends string,\n const BP extends string,\n>(options: Options<NS, PE, BP>): AppStoreConfig<NS, PE, BP> {\n return AppStoreConfig[construct](options);\n}\n"],"mappings":"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"}
|
package/dist/app-store/index.cjs
CHANGED
|
@@ -4,4 +4,6 @@ const require_app_store_handler = require("./handler.cjs");
|
|
|
4
4
|
const require_app_store_mapper = require("./mapper.cjs");
|
|
5
5
|
exports.AppStoreConfig = require_app_store_config.AppStoreConfig;
|
|
6
6
|
exports.AppStoreNotificationHandler = require_app_store_handler.AppStoreNotificationHandler;
|
|
7
|
+
exports.defineAppStoreConfig = require_app_store_config.defineAppStoreConfig;
|
|
7
8
|
exports.mapCancellationDetails = require_app_store_mapper.mapCancellationDetails;
|
|
9
|
+
exports.mapPaymentType = require_app_store_mapper.mapPaymentType;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppStoreConfig } from "./config.cjs";
|
|
1
|
+
import { AppStoreConfig, defineAppStoreConfig } from "./config.cjs";
|
|
2
2
|
import { AppStoreNotificationEvent, AppStoreNotificationHandler } from "./handler.cjs";
|
|
3
|
-
import { AppStoreCancellationSource, mapCancellationDetails } from "./mapper.cjs";
|
|
4
|
-
export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, mapCancellationDetails };
|
|
3
|
+
import { AppStoreCancellationSource, AppStorePaymentSource, mapCancellationDetails, mapPaymentType } from "./mapper.cjs";
|
|
4
|
+
export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, type AppStorePaymentSource, defineAppStoreConfig, mapCancellationDetails, mapPaymentType };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppStoreConfig } from "./config.mjs";
|
|
1
|
+
import { AppStoreConfig, defineAppStoreConfig } from "./config.mjs";
|
|
2
2
|
import { AppStoreNotificationEvent, AppStoreNotificationHandler } from "./handler.mjs";
|
|
3
|
-
import { AppStoreCancellationSource, mapCancellationDetails } from "./mapper.mjs";
|
|
4
|
-
export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, mapCancellationDetails };
|
|
3
|
+
import { AppStoreCancellationSource, AppStorePaymentSource, mapCancellationDetails, mapPaymentType } from "./mapper.mjs";
|
|
4
|
+
export { type AppStoreCancellationSource, AppStoreConfig, type AppStoreNotificationEvent, AppStoreNotificationHandler, type AppStorePaymentSource, defineAppStoreConfig, mapCancellationDetails, mapPaymentType };
|
package/dist/app-store/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppStoreConfig } from "./config.mjs";
|
|
1
|
+
import { AppStoreConfig, defineAppStoreConfig } from "./config.mjs";
|
|
2
2
|
import { AppStoreNotificationHandler } from "./handler.mjs";
|
|
3
|
-
import { mapCancellationDetails } from "./mapper.mjs";
|
|
4
|
-
export { AppStoreConfig, AppStoreNotificationHandler, mapCancellationDetails };
|
|
3
|
+
import { mapCancellationDetails, mapPaymentType } from "./mapper.mjs";
|
|
4
|
+
export { AppStoreConfig, AppStoreNotificationHandler, defineAppStoreConfig, mapCancellationDetails, mapPaymentType };
|
|
@@ -31,6 +31,27 @@ function mapCancellationDetails(source) {
|
|
|
31
31
|
default: return null;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The payment a subscription notification's transaction is, if it is one: the first purchase
|
|
36
|
+
* (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
|
|
37
|
+
* (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
|
|
38
|
+
* full and starts a new period at once, refunding what is left of the old one separately, so it is
|
|
39
|
+
* a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
|
|
40
|
+
* and the like carry the current transaction, already paid, and are null.
|
|
41
|
+
*
|
|
42
|
+
* ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
|
|
43
|
+
*/
|
|
44
|
+
function mapPaymentType({ notificationType, subtype }) {
|
|
45
|
+
switch (notificationType) {
|
|
46
|
+
case "SUBSCRIBED": return "SUBSCRIPTION_START";
|
|
47
|
+
case "DID_RENEW": return "SUBSCRIPTION_RENEWAL";
|
|
48
|
+
case "DID_CHANGE_RENEWAL_PREF": return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
|
|
49
|
+
case "OFFER_REDEEMED":
|
|
50
|
+
if (subtype === "INITIAL_BUY" || subtype === "RESUBSCRIBE") return "SUBSCRIPTION_START";
|
|
51
|
+
return subtype === "UPGRADE" ? "SUBSCRIPTION_RENEWAL" : null;
|
|
52
|
+
default: return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */
|
|
35
56
|
function expirationIntentSubtype(intent) {
|
|
36
57
|
switch (intent) {
|
|
@@ -43,5 +64,6 @@ function expirationIntentSubtype(intent) {
|
|
|
43
64
|
}
|
|
44
65
|
//#endregion
|
|
45
66
|
exports.mapCancellationDetails = mapCancellationDetails;
|
|
67
|
+
exports.mapPaymentType = mapPaymentType;
|
|
46
68
|
|
|
47
69
|
//# sourceMappingURL=mapper.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
|
|
1
|
+
{"version":3,"file":"mapper.cjs","names":[],"sources":["../../src/app-store/mapper.ts"],"sourcesContent":["import type { CancellationDetails, PaymentType } from '../subscription/index';\n\n/**\n * The App Store Server Notification fields that say why a subscription stops renewing: the\n * notification type and subtype, plus `expirationIntent` from the renewal info once it has\n * expired. Strings and numbers as Apple sends them, so this needs no runtime import of the\n * App Store Server Library.\n */\nexport interface AppStoreCancellationSource {\n notificationType: string;\n subtype?: string | null;\n /** `JWSRenewalInfoDecodedPayload.expirationIntent`: 1 customer, 2 billing, 3 price increase, 4 unavailable, 5 other. */\n expirationIntent?: number | null;\n}\n\n/**\n * Who ended an App Store subscription and why, from the notification that reported it. Null for\n * anything that is not a cancellation, including the customer turning auto-renew back on.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n * ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)\n */\nexport function mapCancellationDetails(\n source: AppStoreCancellationSource\n): CancellationDetails | null {\n const { notificationType, subtype, expirationIntent } = source;\n const details = (initiator: CancellationDetails['initiator'], reason: string) => ({\n initiator,\n reason,\n feedback: null,\n comment: null,\n });\n\n switch (notificationType) {\n case 'DID_CHANGE_RENEWAL_STATUS':\n return subtype === 'AUTO_RENEW_DISABLED' ? details('user', subtype) : null;\n case 'EXPIRED':\n switch (subtype ?? expirationIntentSubtype(expirationIntent)) {\n case 'VOLUNTARY':\n return details('user', 'VOLUNTARY');\n case 'BILLING_RETRY':\n return details('system', 'BILLING_RETRY');\n case 'PRICE_INCREASE':\n return details('user', 'PRICE_INCREASE');\n case 'PRODUCT_NOT_FOR_SALE':\n return details('developer', 'PRODUCT_NOT_FOR_SALE');\n default:\n return details('system', 'EXPIRED');\n }\n case 'DID_FAIL_TO_RENEW':\n case 'GRACE_PERIOD_EXPIRED':\n case 'REFUND':\n case 'REVOKE':\n return details('system', notificationType);\n default:\n return null;\n }\n}\n\n/** The App Store Server Notification fields that say whether a notification is a payment. */\nexport interface AppStorePaymentSource {\n notificationType?: string | null;\n subtype?: string | null;\n}\n\n/**\n * The payment a subscription notification's transaction is, if it is one: the first purchase\n * (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal\n * (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in\n * full and starts a new period at once, refunding what is left of the old one separately, so it is\n * a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change\n * and the like carry the current transaction, already paid, and are null.\n *\n * ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)\n */\nexport function mapPaymentType({\n notificationType,\n subtype,\n}: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null {\n switch (notificationType) {\n case 'SUBSCRIBED':\n return 'SUBSCRIPTION_START';\n case 'DID_RENEW':\n return 'SUBSCRIPTION_RENEWAL';\n case 'DID_CHANGE_RENEWAL_PREF':\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n case 'OFFER_REDEEMED':\n if (subtype === 'INITIAL_BUY' || subtype === 'RESUBSCRIBE') return 'SUBSCRIPTION_START';\n return subtype === 'UPGRADE' ? 'SUBSCRIPTION_RENEWAL' : null;\n default:\n return null;\n }\n}\n\n/** The EXPIRED subtype Apple would have sent for an `expirationIntent`. */\nfunction expirationIntentSubtype(intent: number | null | undefined) {\n switch (intent) {\n case 1:\n return 'VOLUNTARY';\n case 2:\n return 'BILLING_RETRY';\n case 3:\n return 'PRICE_INCREASE';\n case 4:\n return 'PRODUCT_NOT_FOR_SALE';\n default:\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;AAsBA,SAAgB,uBACd,QAC4B;CAC5B,MAAM,EAAE,kBAAkB,SAAS,qBAAqB;CACxD,MAAM,WAAW,WAA6C,YAAoB;EAChF;EACA;EACA,UAAU;EACV,SAAS;CACX;CAEA,QAAQ,kBAAR;EACE,KAAK,6BACH,OAAO,YAAY,wBAAwB,QAAQ,QAAQ,OAAO,IAAI;EACxE,KAAK,WACH,QAAQ,WAAW,wBAAwB,gBAAgB,GAA3D;GACE,KAAK,aACH,OAAO,QAAQ,QAAQ,WAAW;GACpC,KAAK,iBACH,OAAO,QAAQ,UAAU,eAAe;GAC1C,KAAK,kBACH,OAAO,QAAQ,QAAQ,gBAAgB;GACzC,KAAK,wBACH,OAAO,QAAQ,aAAa,sBAAsB;GACpD,SACE,OAAO,QAAQ,UAAU,SAAS;EACtC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,OAAO,QAAQ,UAAU,gBAAgB;EAC3C,SACE,OAAO;CACX;AACF;;;;;;;;;;;AAkBA,SAAgB,eAAe,EAC7B,kBACA,WACiE;CACjE,QAAQ,kBAAR;EACE,KAAK,cACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,2BACH,OAAO,YAAY,YAAY,yBAAyB;EAC1D,KAAK;GACH,IAAI,YAAY,iBAAiB,YAAY,eAAe,OAAO;GACnE,OAAO,YAAY,YAAY,yBAAyB;EAC1D,SACE,OAAO;CACX;AACF;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,QAAQ,QAAR;EACE,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE;CACJ;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CancellationDetails } from "../subscription/index.cjs";
|
|
1
|
+
import { CancellationDetails, PaymentType } from "../subscription/index.cjs";
|
|
2
2
|
//#region src/app-store/mapper.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* The App Store Server Notification fields that say why a subscription stops renewing: the
|
|
@@ -20,5 +20,21 @@ export interface AppStoreCancellationSource {
|
|
|
20
20
|
* ref: [expirationIntent](https://developer.apple.com/documentation/appstoreserverapi/expirationintent)
|
|
21
21
|
*/
|
|
22
22
|
export declare function mapCancellationDetails(source: AppStoreCancellationSource): CancellationDetails | null;
|
|
23
|
+
/** The App Store Server Notification fields that say whether a notification is a payment. */
|
|
24
|
+
export interface AppStorePaymentSource {
|
|
25
|
+
notificationType?: string | null;
|
|
26
|
+
subtype?: string | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The payment a subscription notification's transaction is, if it is one: the first purchase
|
|
30
|
+
* (`SUBSCRIBED`, or a redeemed offer code that starts or resumes the subscription), a renewal
|
|
31
|
+
* (`DID_RENEW`), or an upgrade. An upgrade counts as a renewal: Apple charges the higher plan in
|
|
32
|
+
* full and starts a new period at once, refunding what is left of the old one separately, so it is
|
|
33
|
+
* a regular period charge rather than an off-cycle proration. A downgrade, an auto-renew change
|
|
34
|
+
* and the like carry the current transaction, already paid, and are null.
|
|
35
|
+
*
|
|
36
|
+
* ref: [notificationType](https://developer.apple.com/documentation/appstoreservernotifications/notificationtype)
|
|
37
|
+
*/
|
|
38
|
+
export declare function mapPaymentType({ notificationType, subtype }: AppStorePaymentSource): Exclude<PaymentType, 'ONE_TIME'> | null;
|
|
23
39
|
//#endregion
|
|
24
40
|
//# sourceMappingURL=mapper.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP"}
|
|
1
|
+
{"version":3,"file":"mapper.d.cts","names":[],"sources":["../../src/app-store/mapper.ts"],"mappings":";;;;;;;;iBAQiB;EACf;EACA;;EAEA;;;;;;;;;wBAUc,uBACd,QAAQ,6BACP;;iBAoCc;EACf;EACA;;;;;;;;;;;;wBAac,iBACd,kBACA,WACC,wBAAwB,QAAQ"}
|