@shware/purchase 5.1.1 → 5.2.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 +341 -0
- package/dist/app-store/config.d.cts +1 -0
- package/dist/app-store/config.d.mts +1 -0
- package/dist/app-store/handler.d.cts +1 -0
- package/dist/app-store/handler.d.mts +1 -0
- package/dist/google-play/config.d.cts +1 -0
- package/dist/google-play/config.d.mts +1 -0
- package/dist/stripe/config.d.cts +1 -0
- package/dist/stripe/config.d.mts +1 -0
- package/dist/stripe/handler.d.cts +1 -0
- package/dist/stripe/handler.d.mts +1 -0
- package/package.json +9 -7
package/README.md
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# @shware/purchase
|
|
2
|
+
|
|
3
|
+
Shared building blocks for selling subscriptions and credits on Stripe, the App Store and Google
|
|
4
|
+
Play. The root export holds a platform-neutral subscription model (statuses, cancellation details)
|
|
5
|
+
and the three product catalogs (`StripeConfig`, `AppStoreConfig`, `GooglePlayConfig`) that map
|
|
6
|
+
store product ids to your plans, billing periods and credit grants. The platform subpaths add
|
|
7
|
+
typed event dispatchers and mappers from each store's payloads onto that model. Verifying webhook
|
|
8
|
+
signatures and calling store APIs stays in your app, with the official SDKs.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add @shware/purchase zod
|
|
14
|
+
# optional peers, only for the subpaths you use
|
|
15
|
+
pnpm add stripe # @shware/purchase/stripe (^22.6.2 || ^23.0.0)
|
|
16
|
+
pnpm add @apple/app-store-server-library # @shware/purchase/app-store (^3.1.0)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Entry points
|
|
20
|
+
|
|
21
|
+
| Import path | Contents |
|
|
22
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `@shware/purchase` | Subscription model (`PLATFORMS`, `SUBSCRIPTION_STATUSES`, `AVAILABLE_STATUSES`, cancellation types) and the three configs |
|
|
24
|
+
| `@shware/purchase/stripe` | `StripeEventHandler`, Stripe mappers, checkout schemas, `METADATA_KEYS`, analytics helpers, browser pending-checkout helpers |
|
|
25
|
+
| `@shware/purchase/app-store` | `AppStoreNotificationHandler` for App Store Server Notifications V2, `mapCancellationDetails` |
|
|
26
|
+
| `@shware/purchase/google-play` | Real-time developer notification (RTDN) types and enums, subscription state and cancellation mappers |
|
|
27
|
+
|
|
28
|
+
## Subscription model
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import {
|
|
32
|
+
AVAILABLE_STATUSES,
|
|
33
|
+
PLATFORMS,
|
|
34
|
+
SUBSCRIPTION_STATUSES,
|
|
35
|
+
type CancellationDetails,
|
|
36
|
+
type Platform,
|
|
37
|
+
type SubscriptionStatus,
|
|
38
|
+
} from '@shware/purchase';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `PLATFORMS`: `'APPLE' | 'GOOGLE' | 'STRIPE'`.
|
|
42
|
+
- `SUBSCRIPTION_STATUSES`: the union of every store's states (`TRIALING`, `ACTIVE`,
|
|
43
|
+
`IN_GRACE_PERIOD`, `PAST_DUE`, `ON_HOLD`, `PAUSED`, `CANCELED`, `EXPIRED`, ...). Each platform
|
|
44
|
+
mapper returns one of these.
|
|
45
|
+
- `AVAILABLE_STATUSES`: `ACTIVE`, `TRIALING`, `IN_GRACE_PERIOD`, the statuses that grant access.
|
|
46
|
+
- `CancellationDetails`: why a subscription stopped renewing, one record for all platforms:
|
|
47
|
+
`initiator` (`CANCELLATION_INITIATORS`: `user`, `system`, `developer`), the store's own `reason`,
|
|
48
|
+
`feedback` (`CANCELLATION_FEEDBACKS`), `comment` and `resubscribeIntent` (`RESUBSCRIBE_INTENTS`).
|
|
49
|
+
|
|
50
|
+
The constants are `as const` arrays, so they can back database enums directly:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { pgEnum } from 'drizzle-orm/pg-core';
|
|
54
|
+
import { PLATFORMS, SUBSCRIPTION_STATUSES } from '@shware/purchase';
|
|
55
|
+
|
|
56
|
+
export const platform = pgEnum('platform', PLATFORMS);
|
|
57
|
+
export const subscriptionStatus = pgEnum('subscription_status', SUBSCRIPTION_STATUSES);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const entitled = (AVAILABLE_STATUSES as readonly SubscriptionStatus[]).includes(sub.status);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Product catalogs
|
|
65
|
+
|
|
66
|
+
Each config is built with a fluent, type-checked chain. Type parameters are the product id
|
|
67
|
+
namespace, your plan type and your billing period type; product ids must start with the namespace,
|
|
68
|
+
and duplicate products or plan/period pairs fail at compile time and at runtime. Credit metadata
|
|
69
|
+
is `{ credits: number; expiresIn: string }` where `expiresIn` is an
|
|
70
|
+
[`ms`](https://github.com/vercel/ms) duration.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { AppStoreConfig, GooglePlayConfig, StripeConfig } from '@shware/purchase';
|
|
74
|
+
|
|
75
|
+
type Plan = 'STARTER' | 'PRO';
|
|
76
|
+
type BillingPeriod = 'MONTHLY' | 'YEARLY';
|
|
77
|
+
|
|
78
|
+
export const stripe = StripeConfig.create<'com.example.stripe', Plan, BillingPeriod>({
|
|
79
|
+
mode: env.STRIPE_SECRET_KEY.startsWith('sk_test_') ? 'test' : 'live',
|
|
80
|
+
allowPromotionCodes: false,
|
|
81
|
+
cancellationCouponId: 'retention_30pct_3mo',
|
|
82
|
+
returnUrl: 'https://example.com',
|
|
83
|
+
cancelUrl: 'https://example.com',
|
|
84
|
+
successUrl: 'https://example.com/payments/success?session_id={CHECKOUT_SESSION_ID}',
|
|
85
|
+
})
|
|
86
|
+
// one-time product: no plan or period
|
|
87
|
+
.product('com.example.stripe.credit.starter')
|
|
88
|
+
.price('price_live_credits', { credits: 10, expiresIn: '1 year' })
|
|
89
|
+
.default('price_live_credits', { test: 'price_test_credits' })
|
|
90
|
+
// subscription: plan + billing period
|
|
91
|
+
.product('com.example.stripe.sub.monthly.starter', 'STARTER', 'MONTHLY')
|
|
92
|
+
.price('price_live_starter_v2', { credits: 100, expiresIn: '30 days' })
|
|
93
|
+
.price('price_live_starter_v1', { credits: 100, expiresIn: '30 days' }) // grandfathered
|
|
94
|
+
.default('price_live_starter_v2', { test: 'price_test_starter' });
|
|
95
|
+
|
|
96
|
+
export const appStore = AppStoreConfig.create<'com.example.appstore', Plan, BillingPeriod>({
|
|
97
|
+
appId: '1234567890',
|
|
98
|
+
bundleId: 'com.example.app',
|
|
99
|
+
})
|
|
100
|
+
.consumable('com.example.appstore.credit.starter', { credits: 10, expiresIn: '365d' })
|
|
101
|
+
.subscription('STARTER', 'MONTHLY')
|
|
102
|
+
.product('com.example.appstore.sub.monthly.starter.v1', { credits: 100, expiresIn: '30d' })
|
|
103
|
+
.default('com.example.appstore.sub.monthly.starter.v1');
|
|
104
|
+
|
|
105
|
+
export const googlePlay = GooglePlayConfig.create<'com.example.googleplay', Plan, BillingPeriod>({
|
|
106
|
+
package: 'com.example.app',
|
|
107
|
+
audience: 'api.example.com', // expected audience of the RTDN push OIDC token
|
|
108
|
+
})
|
|
109
|
+
.subscription('com.example.googleplay.sub.starter')
|
|
110
|
+
.basePlan('monthly-v1', 'STARTER', 'MONTHLY', { credits: 100, expiresIn: '30d' })
|
|
111
|
+
.basePlan('yearly-v1', 'STARTER', 'YEARLY', { credits: 1200, expiresIn: '365d' })
|
|
112
|
+
.default(['monthly-v1', 'yearly-v1'])
|
|
113
|
+
.onetime('com.example.googleplay.credit.starter', { credits: 10, expiresIn: '365d' });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Lookups:
|
|
117
|
+
|
|
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?)` |
|
|
123
|
+
|
|
124
|
+
In Stripe test mode the `test` twin of the default price is what `getPriceId` returns, and it
|
|
125
|
+
resolves to the default price's credits. Non-default prices, products and base plans stay declared
|
|
126
|
+
so existing subscribers keep resolving.
|
|
127
|
+
|
|
128
|
+
## Stripe
|
|
129
|
+
|
|
130
|
+
`@shware/purchase/stripe` (requires `stripe`).
|
|
131
|
+
|
|
132
|
+
### Webhooks
|
|
133
|
+
|
|
134
|
+
Verify the signature with the Stripe SDK, then dispatch with `StripeEventHandler`. `on` is typed
|
|
135
|
+
per event, so each handler receives the right `event.data.object`; `process` throws for an event
|
|
136
|
+
type with no handler.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import Stripe from 'stripe';
|
|
140
|
+
import {
|
|
141
|
+
METADATA_KEYS,
|
|
142
|
+
StripeEventHandler,
|
|
143
|
+
mapCancellationDetails,
|
|
144
|
+
mapSubscriptionStatus,
|
|
145
|
+
mapTime,
|
|
146
|
+
} from '@shware/purchase/stripe';
|
|
147
|
+
|
|
148
|
+
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
|
|
149
|
+
|
|
150
|
+
export async function processStripeEvent(signature: string, body: string) {
|
|
151
|
+
const event = await stripe.webhooks.constructEventAsync(
|
|
152
|
+
body,
|
|
153
|
+
signature,
|
|
154
|
+
env.STRIPE_WEBHOOK_SECRET
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
await StripeEventHandler.create()
|
|
158
|
+
.on('checkout.session.completed', handleCheckoutSessionCompleted)
|
|
159
|
+
.on('invoice.payment_succeeded', handleInvoicePaymentSucceeded)
|
|
160
|
+
.on('customer.subscription.created', handleSubscriptionEvent)
|
|
161
|
+
.on('customer.subscription.updated', handleSubscriptionEvent)
|
|
162
|
+
.on('customer.subscription.deleted', handleSubscriptionEvent)
|
|
163
|
+
.process(event);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function handleSubscriptionEvent(sub: Stripe.Subscription) {
|
|
167
|
+
const userId = sub.metadata[METADATA_KEYS.USER_ID];
|
|
168
|
+
const productId = sub.metadata[METADATA_KEYS.PRODUCT_ID];
|
|
169
|
+
await saveSubscription({
|
|
170
|
+
userId,
|
|
171
|
+
platform: 'STRIPE',
|
|
172
|
+
plan: config.stripe.getPlan(productId),
|
|
173
|
+
billingPeriod: config.stripe.getBillingPeriod(productId),
|
|
174
|
+
status: mapSubscriptionStatus(sub.status),
|
|
175
|
+
startedAt: mapTime(sub.start_date),
|
|
176
|
+
cancellationDetails: mapCancellationDetails(sub),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Checkout
|
|
182
|
+
|
|
183
|
+
`createCheckoutSessionSchema(productIds)` is a `zod/mini` schema for the create-checkout request
|
|
184
|
+
(`productId`, `quantity` default 1, optional `cancelUrl` / `successUrl`). `METADATA_KEYS` names
|
|
185
|
+
the metadata fields (`user_id`, `product_id`, `referral_code`, ...) the webhook reads back.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { zValidator } from '@hono/zod-validator';
|
|
189
|
+
import { METADATA_KEYS, createCheckoutSessionSchema } from '@shware/purchase/stripe';
|
|
190
|
+
|
|
191
|
+
app.post(
|
|
192
|
+
'/v1/checkout/sessions',
|
|
193
|
+
zValidator('json', createCheckoutSessionSchema(config.stripe.productIds)),
|
|
194
|
+
async (c) => {
|
|
195
|
+
const { productId, quantity, cancelUrl, successUrl } = c.req.valid('json');
|
|
196
|
+
const userId = await getUserId(c);
|
|
197
|
+
const session = await stripe.checkout.sessions.create({
|
|
198
|
+
mode: config.stripe.getMode(productId),
|
|
199
|
+
line_items: [{ price: config.stripe.getPriceId(productId), quantity }],
|
|
200
|
+
metadata: { [METADATA_KEYS.USER_ID]: userId, [METADATA_KEYS.PRODUCT_ID]: productId },
|
|
201
|
+
allow_promotion_codes: config.stripe.allowPromotionCodes,
|
|
202
|
+
success_url: successUrl ?? config.stripe.successUrl,
|
|
203
|
+
cancel_url: cancelUrl ?? config.stripe.cancelUrl,
|
|
204
|
+
});
|
|
205
|
+
return c.json({ id: session.id, url: session.url });
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`cancellationDetailsSchema` validates what your own cancel dialog sends (`comment`, `feedback`,
|
|
211
|
+
`resubscribeIntent`).
|
|
212
|
+
|
|
213
|
+
### Mappers
|
|
214
|
+
|
|
215
|
+
Plain-JSON views of Stripe objects for API responses: `mapCheckoutSession`, `mapLineItem`,
|
|
216
|
+
`mapPrice`, `mapInvoice`, `mapPaymentIntent`, `mapCharge`, plus `mapSubscriptionStatus`,
|
|
217
|
+
`mapCancellationDetails` and `mapTime` (Unix seconds to ISO string, `null` passes through).
|
|
218
|
+
|
|
219
|
+
### Browser helpers
|
|
220
|
+
|
|
221
|
+
These run in the browser and have no runtime dependency on `stripe`:
|
|
222
|
+
|
|
223
|
+
- `price(value, currency)` / `minorUnits(currency)`: convert minor units to a display amount,
|
|
224
|
+
aware of zero-decimal currencies.
|
|
225
|
+
- `getBeginCheckoutProperties(productPrice)` and `getPurchaseProperties(checkoutSession)`:
|
|
226
|
+
GA4-style `begin_checkout` / `purchase` event properties.
|
|
227
|
+
- `addPendingCheckout(sessionId)`, `getPendingCheckouts()`, `removePendingCheckout(sessionId)`:
|
|
228
|
+
a `localStorage` list (one-hour TTL) of checkouts to re-confirm if the user closed the tab before
|
|
229
|
+
returning to the success page.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import {
|
|
233
|
+
addPendingCheckout,
|
|
234
|
+
getPendingCheckouts,
|
|
235
|
+
removePendingCheckout,
|
|
236
|
+
} from '@shware/purchase/stripe';
|
|
237
|
+
|
|
238
|
+
// before redirecting to Stripe
|
|
239
|
+
addPendingCheckout(session.id);
|
|
240
|
+
|
|
241
|
+
// on app start
|
|
242
|
+
for (const sessionId of getPendingCheckouts()) {
|
|
243
|
+
await confirmCheckoutSession(sessionId);
|
|
244
|
+
removePendingCheckout(sessionId);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## App Store
|
|
249
|
+
|
|
250
|
+
`@shware/purchase/app-store` (requires `@apple/app-store-server-library`).
|
|
251
|
+
|
|
252
|
+
Decode and verify the notification with Apple's `SignedDataVerifier`, then dispatch the decoded
|
|
253
|
+
event by `notificationType` with `AppStoreNotificationHandler`. Branch on `subtype` inside each
|
|
254
|
+
handler.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { NotificationTypeV2, type SignedDataVerifier } from '@apple/app-store-server-library';
|
|
258
|
+
import {
|
|
259
|
+
type AppStoreNotificationEvent,
|
|
260
|
+
AppStoreNotificationHandler,
|
|
261
|
+
mapCancellationDetails,
|
|
262
|
+
} from '@shware/purchase/app-store';
|
|
263
|
+
|
|
264
|
+
export async function processAppStoreNotification(verifier: SignedDataVerifier, body: string) {
|
|
265
|
+
const payload = await verifier.verifyAndDecodeNotification(body);
|
|
266
|
+
const { signedTransactionInfo, signedRenewalInfo } = payload.data ?? {};
|
|
267
|
+
const transaction = signedTransactionInfo
|
|
268
|
+
? await verifier.verifyAndDecodeTransaction(signedTransactionInfo)
|
|
269
|
+
: null;
|
|
270
|
+
const renewalInfo = signedRenewalInfo
|
|
271
|
+
? await verifier.verifyAndDecodeRenewalInfo(signedRenewalInfo)
|
|
272
|
+
: null;
|
|
273
|
+
|
|
274
|
+
await AppStoreNotificationHandler.create()
|
|
275
|
+
.on(NotificationTypeV2.SUBSCRIBED, handleSubscribed)
|
|
276
|
+
.on(NotificationTypeV2.DID_RENEW, handleRenewed)
|
|
277
|
+
.on(NotificationTypeV2.DID_CHANGE_RENEWAL_STATUS, handleRenewalStatusChanged)
|
|
278
|
+
.on(NotificationTypeV2.EXPIRED, handleExpired)
|
|
279
|
+
.on(NotificationTypeV2.REFUND, handleRefunded)
|
|
280
|
+
.process({ payload, transaction, renewalInfo });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function handleRenewalStatusChanged({ payload, renewalInfo }: AppStoreNotificationEvent) {
|
|
284
|
+
const cancellationDetails = mapCancellationDetails({
|
|
285
|
+
notificationType: payload.notificationType!,
|
|
286
|
+
subtype: payload.subtype,
|
|
287
|
+
expirationIntent: renewalInfo?.expirationIntent,
|
|
288
|
+
});
|
|
289
|
+
// null when auto-renew was turned back on
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Map `transaction.productId` to your plan with `config.appStore.getPlan(productId)` and
|
|
294
|
+
`getBillingPeriod(productId)`, and grant credits with `getCreditAmount` / `getCreditExpiresAt`.
|
|
295
|
+
|
|
296
|
+
## Google Play
|
|
297
|
+
|
|
298
|
+
`@shware/purchase/google-play` (no peer dependency).
|
|
299
|
+
|
|
300
|
+
It provides the RTDN payload types (`PubsubMessage`, `DeveloperNotification`,
|
|
301
|
+
`SubscriptionNotification`, `OneTimeProductNotification`, `VoidedPurchaseNotification`,
|
|
302
|
+
`TestNotification`), the numeric enums (`SubscriptionNotificationType`,
|
|
303
|
+
`OneTimeProductNotificationType`, `VoidedPurchaseProductType`, `VoidedPurchaseRefundType`), and
|
|
304
|
+
mappers for the `purchases.subscriptionsv2` resource. Verify the Pub/Sub push token (its audience
|
|
305
|
+
is `config.googlePlay.audience`) and fetch the purchase with the Android Publisher API yourself.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import {
|
|
309
|
+
type DeveloperNotification,
|
|
310
|
+
type GooglePlaySubscriptionState,
|
|
311
|
+
type PubsubMessage,
|
|
312
|
+
mapCanceledAt,
|
|
313
|
+
mapCancellationDetails,
|
|
314
|
+
mapSubscriptionStatus,
|
|
315
|
+
} from '@shware/purchase/google-play';
|
|
316
|
+
|
|
317
|
+
export async function processGooglePlayNotification(authorization: string, body: string) {
|
|
318
|
+
await verifyIdToken(authorization.replace('Bearer ', '')); // your OIDC check
|
|
319
|
+
|
|
320
|
+
const { message } = JSON.parse(body) as { message: PubsubMessage };
|
|
321
|
+
const notification = JSON.parse(
|
|
322
|
+
Buffer.from(message.data, 'base64').toString('utf-8')
|
|
323
|
+
) as Partial<DeveloperNotification>;
|
|
324
|
+
|
|
325
|
+
if (notification.subscriptionNotification) {
|
|
326
|
+
const { purchaseToken } = notification.subscriptionNotification;
|
|
327
|
+
const { data } = await publisher.purchases.subscriptionsv2.get({
|
|
328
|
+
packageName: config.googlePlay.package,
|
|
329
|
+
token: purchaseToken,
|
|
330
|
+
});
|
|
331
|
+
const status = mapSubscriptionStatus(data.subscriptionState as GooglePlaySubscriptionState);
|
|
332
|
+
const cancellationDetails = mapCancellationDetails(data.canceledStateContext);
|
|
333
|
+
const canceledAt = mapCanceledAt(data.canceledStateContext);
|
|
334
|
+
// save status, cancellationDetails, canceledAt ...
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Resolve the plan from a line item with `config.googlePlay.getPlan(productId, basePlanId)` and
|
|
340
|
+
`getBillingPeriod(productId, basePlanId)`. `GOOGLE_PLAY_SUBSCRIPTION_STATES` lists the raw
|
|
341
|
+
`SUBSCRIPTION_STATE_*` values.
|
package/dist/stripe/config.d.cts
CHANGED
package/dist/stripe/config.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shware/purchase",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,20 +56,22 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@shware/utils": "^1.6.
|
|
59
|
+
"@shware/utils": "^1.6.3",
|
|
60
60
|
"@types/ms": "^2.1.0",
|
|
61
|
-
"ms": "^2.1.3"
|
|
62
|
-
"zod": "^4.6.5"
|
|
61
|
+
"ms": "^2.1.3"
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@repo/typescript-config": "0.0.0",
|
|
66
|
-
"@types/node": "^26.6.
|
|
65
|
+
"@types/node": "^26.6.5",
|
|
66
|
+
"stripe": "^23.0.0",
|
|
67
67
|
"typescript": "^7.0.2",
|
|
68
|
-
"vitest": "^5.0.
|
|
68
|
+
"vitest": "^5.0.5",
|
|
69
|
+
"zod": "^4.6.7"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
72
|
"@apple/app-store-server-library": "^3.1.0",
|
|
72
|
-
"stripe": "^22.6.2"
|
|
73
|
+
"stripe": "^22.6.2 || ^23.0.0",
|
|
74
|
+
"zod": "^4.6.5"
|
|
73
75
|
},
|
|
74
76
|
"peerDependenciesMeta": {
|
|
75
77
|
"@apple/app-store-server-library": {
|