expo-creem-integration 1.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/LICENSE +21 -0
- package/README.md +593 -0
- package/app.plugin.js +1 -0
- package/package.json +57 -0
- package/plugin/index.js +115 -0
- package/src/components/CreemCheckoutButton.tsx +182 -0
- package/src/components/SubscriptionStatus.tsx +272 -0
- package/src/components/index.ts +4 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCreemCheckout.ts +216 -0
- package/src/hooks/useCreemCustomerPortal.ts +87 -0
- package/src/hooks/useCreemLicense.ts +137 -0
- package/src/hooks/useCreemProducts.ts +134 -0
- package/src/hooks/useCreemSubscription.ts +300 -0
- package/src/index.ts +69 -0
- package/src/launcher.ts +122 -0
- package/src/server/creem-server.ts +478 -0
- package/src/server/index.ts +10 -0
- package/src/types/index.ts +490 -0
- package/src/utils/client.ts +507 -0
- package/src/utils/context.tsx +175 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# expo-creem
|
|
2
|
+
|
|
3
|
+
Creem payment integration for Expo apps. Launch checkout sessions, manage subscriptions, handle licenses, open customer portals, and handle deep-link callbacks — all without ejecting or writing native code.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://docs.expo.dev/)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Checkout Sessions** — open the Creem-hosted checkout via `expo-web-browser` and receive the result via deep link
|
|
11
|
+
- **Subscription Management** — fetch, cancel, update, upgrade, pause, and resume subscriptions
|
|
12
|
+
- **License Key Management** — activate, validate, and deactivate software licenses
|
|
13
|
+
- **Customer Portal** — open the Creem billing portal for customers to manage payment methods
|
|
14
|
+
- **Products & Discounts** — search products, retrieve discounts
|
|
15
|
+
- **React Hooks** — `useCreemCheckout`, `useCreemCheckoutWithDeeplink`, `useCreemSubscription`, `useCreemProducts`, `useCreemLicense`, `useCreemCustomerPortal`
|
|
16
|
+
- **Pre-built Components** — `<CreemCheckoutButton>`, `<SubscriptionStatus>`, `<SubscriptionBadge>`
|
|
17
|
+
- **Expo Config Plugin** — zero-config URL scheme setup via `expo prebuild`
|
|
18
|
+
- **Server-side Helpers** — Node.js / Edge-compatible client for your backend with retry logic
|
|
19
|
+
- **Webhook Support** — signature verification and typed event handlers
|
|
20
|
+
- **Utility Helpers** — `formatPrice`, `formatDate`, `formatBillingPeriod`, `formatRelativeTime`, `isSubscriptionActive`
|
|
21
|
+
- **TypeScript First** — all types match the Creem OpenAPI spec exactly
|
|
22
|
+
- **Cross-platform** — iOS, Android, and Expo Web
|
|
23
|
+
- **Retry Logic** — built-in exponential backoff for resilient API calls
|
|
24
|
+
- **Performance** — `React.memo`, `useMemo`, `useCallback` throughout; cleanup in all effects
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Expo SDK 52+
|
|
29
|
+
- Node.js 18+
|
|
30
|
+
- A Creem account ([creem.io](https://creem.io))
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx expo install expo-creem expo-web-browser expo-linking
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
### 1. Add the config plugin
|
|
41
|
+
|
|
42
|
+
In `app.json` / `app.config.js`, add `expo-creem` to the `plugins` array. The plugin automatically registers your URL scheme on iOS and Android so deep links from the Creem checkout page reach your app.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"expo": {
|
|
47
|
+
"scheme": "myapp",
|
|
48
|
+
"plugins": [
|
|
49
|
+
["expo-creem", { "scheme": "myapp" }]
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then run `expo prebuild` to apply the native changes.
|
|
56
|
+
|
|
57
|
+
> **Note**: If you omit the `scheme` option, the plugin reads the top-level `expo.scheme` field automatically.
|
|
58
|
+
|
|
59
|
+
### 2. Wrap your app with `<CreemProvider>`
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { CreemProvider } from 'expo-creem';
|
|
63
|
+
|
|
64
|
+
export default function App() {
|
|
65
|
+
return (
|
|
66
|
+
<CreemProvider apiKey="YOUR_API_KEY" environment="sandbox">
|
|
67
|
+
{/* your app */}
|
|
68
|
+
</CreemProvider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Use `environment="sandbox"` for testing (points at `https://test-api.creem.io`) and `environment="production"` (or omit it) for live payments.
|
|
74
|
+
|
|
75
|
+
#### Provider Options
|
|
76
|
+
|
|
77
|
+
| Prop | Type | Default | Description |
|
|
78
|
+
|---|---|---|---|
|
|
79
|
+
| `apiKey` | `string` | **required** | Your Creem API key |
|
|
80
|
+
| `environment` | `'production' \| 'sandbox'` | `'production'` | Which API environment to use |
|
|
81
|
+
| `baseUrl` | `string` | — | Override the base URL (e.g. proxy through your own backend) |
|
|
82
|
+
| `retries` | `number` | `2` | Number of retry attempts for failed requests |
|
|
83
|
+
| `retryDelay` | `number` | `300` | Base delay in ms for exponential backoff |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Launch a checkout with the pre-built button
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { CreemCheckoutButton } from 'expo-creem';
|
|
93
|
+
|
|
94
|
+
<CreemCheckoutButton
|
|
95
|
+
options={{
|
|
96
|
+
product_id: 'prod_xxx',
|
|
97
|
+
success_url: 'myapp://creem/success',
|
|
98
|
+
customer: { email: 'user@example.com' },
|
|
99
|
+
}}
|
|
100
|
+
title="Subscribe Now"
|
|
101
|
+
loadingTitle="Opening checkout..."
|
|
102
|
+
variant="primary" // 'primary' | 'secondary' | 'outline'
|
|
103
|
+
size="large" // 'small' | 'medium' | 'large'
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Launch a checkout with the hook
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useCreemCheckout } from 'expo-creem';
|
|
111
|
+
|
|
112
|
+
function SubscribeButton() {
|
|
113
|
+
const { status, error, startCheckout, reset } = useCreemCheckout({
|
|
114
|
+
product_id: 'prod_xxx',
|
|
115
|
+
success_url: 'myapp://creem/success',
|
|
116
|
+
onComplete: (session) => console.log('Done', session.id),
|
|
117
|
+
onCancel: () => console.log('Cancelled'),
|
|
118
|
+
onError: (err) => console.error(err),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Button
|
|
123
|
+
onPress={startCheckout}
|
|
124
|
+
disabled={status === 'loading'}
|
|
125
|
+
title={status === 'loading' ? 'Loading...' : 'Subscribe'}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Deep-link variant (for external routers)
|
|
132
|
+
|
|
133
|
+
`useCreemCheckoutWithDeeplink` opens the browser and then resolves via an incoming URL event rather than blocking. Useful with Expo Router or React Navigation.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useCreemCheckoutWithDeeplink } from 'expo-creem';
|
|
137
|
+
|
|
138
|
+
const { startCheckout, status } = useCreemCheckoutWithDeeplink({
|
|
139
|
+
product_id: 'prod_xxx',
|
|
140
|
+
success_url: 'myapp://creem/success',
|
|
141
|
+
onComplete: (session) => { /* ... */ },
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Show subscription status
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { SubscriptionStatus, SubscriptionBadge } from 'expo-creem';
|
|
149
|
+
|
|
150
|
+
// Full status with custom renderers
|
|
151
|
+
<SubscriptionStatus
|
|
152
|
+
subscriptionId="sub_xxx"
|
|
153
|
+
showDetails
|
|
154
|
+
pollInterval={30_000}
|
|
155
|
+
onStatusChange={(s) => console.log('Status changed:', s)}
|
|
156
|
+
renderActive={(sub) => (
|
|
157
|
+
<Text>Active — renews {new Date(sub.current_period_end_date).toLocaleDateString()}</Text>
|
|
158
|
+
)}
|
|
159
|
+
renderTrialing={(sub) => <Text>Free trial active!</Text>}
|
|
160
|
+
renderCanceling={(sub) => <Text>Subscription ending soon</Text>}
|
|
161
|
+
renderPaused={() => <Text>Subscription paused</Text>}
|
|
162
|
+
renderInactive={() => <Text>No active subscription</Text>}
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
// Compact badge
|
|
166
|
+
<SubscriptionBadge subscriptionId="sub_xxx" pollInterval={30_000} />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Manage subscriptions
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useCreemSubscription } from 'expo-creem';
|
|
173
|
+
|
|
174
|
+
const { subscription, status, isLoading, cancelSubscription, updateSubscription, upgradeSubscription, pauseSubscription, resumeSubscription } =
|
|
175
|
+
useCreemSubscription('sub_xxx');
|
|
176
|
+
|
|
177
|
+
// Cancel at end of billing period:
|
|
178
|
+
await cancelSubscription({ mode: 'scheduled', onExecute: 'cancel' });
|
|
179
|
+
|
|
180
|
+
// Cancel immediately:
|
|
181
|
+
await cancelSubscription({ mode: 'immediate' });
|
|
182
|
+
|
|
183
|
+
// Update seat count:
|
|
184
|
+
await updateSubscription({
|
|
185
|
+
items: [{ id: 'item_xxx', units: 5 }],
|
|
186
|
+
update_behavior: 'proration-charge-immediately',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Upgrade to a different product:
|
|
190
|
+
await upgradeSubscription({
|
|
191
|
+
product_id: 'prod_premium',
|
|
192
|
+
update_behavior: 'proration-charge-immediately',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Pause/resume:
|
|
196
|
+
await pauseSubscription();
|
|
197
|
+
await resumeSubscription();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Browse products
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { useCreemProducts } from 'expo-creem';
|
|
204
|
+
|
|
205
|
+
const { products, isLoading, hasMore, loadMore, refetch } = useCreemProducts({
|
|
206
|
+
page: 1,
|
|
207
|
+
pageSize: 10,
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### License key management
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { useCreemLicense } from 'expo-creem';
|
|
215
|
+
|
|
216
|
+
const { license, status, isLoading, activate, validate, deactivate, reset } = useCreemLicense();
|
|
217
|
+
|
|
218
|
+
// Activate a license on first use:
|
|
219
|
+
await activate({ key: 'license_key_here', instance_name: 'my-macbook-pro' });
|
|
220
|
+
|
|
221
|
+
// Validate on app startup:
|
|
222
|
+
await validate({ key: 'license_key_here', instance_id: 'inst_xxx' });
|
|
223
|
+
|
|
224
|
+
// Deactivate when switching devices:
|
|
225
|
+
await deactivate({ key: 'license_key_here', instance_id: 'inst_xxx' });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Customer portal
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { useCreemCustomerPortal } from 'expo-creem';
|
|
232
|
+
|
|
233
|
+
const { openPortal, isLoading } = useCreemCustomerPortal('cust_xxx');
|
|
234
|
+
|
|
235
|
+
// Opens an in-app browser with the Creem billing portal:
|
|
236
|
+
await openPortal();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Utility helpers
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { formatPrice, formatDate, formatRelativeTime, isSubscriptionActive } from 'expo-creem';
|
|
243
|
+
|
|
244
|
+
formatPrice(1999, 'USD'); // "$19.99"
|
|
245
|
+
formatPrice(1999, 'EUR', 'de-DE'); // "19,99 €"
|
|
246
|
+
formatDate('2026-03-30T00:00:00Z'); // "Mar 30, 2026"
|
|
247
|
+
formatRelativeTime('2026-04-30T00:00:00Z'); // "in 31 days"
|
|
248
|
+
isSubscriptionActive('active'); // true
|
|
249
|
+
isSubscriptionActive('canceled'); // false
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## API Reference
|
|
255
|
+
|
|
256
|
+
### Hooks
|
|
257
|
+
|
|
258
|
+
#### `useCreemCheckout(options)` / `useCreemCheckoutWithDeeplink(options)`
|
|
259
|
+
|
|
260
|
+
| Field | Type | Description |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `product_id` | `string` | **Required.** The Creem product ID |
|
|
263
|
+
| `customer` | `{ id?: string; email?: string }` | Pre-fill customer info |
|
|
264
|
+
| `units` | `number` | Quantity |
|
|
265
|
+
| `discount_code` | `string` | Pre-fill a discount code |
|
|
266
|
+
| `success_url` | `string` | Deep-link URL to redirect to on success |
|
|
267
|
+
| `request_id` | `string` | Idempotency key |
|
|
268
|
+
| `metadata` | `Record<string, string>` | Arbitrary metadata |
|
|
269
|
+
| `custom_fields` | `CreemCustomFieldRequest[]` | Custom field definitions |
|
|
270
|
+
| `onComplete` | `(session) => void` | Called after a successful checkout |
|
|
271
|
+
| `onCancel` | `() => void` | Called when the user cancels |
|
|
272
|
+
| `onError` | `(error) => void` | Called on error |
|
|
273
|
+
| `autoCloseDelay` | `number` | Delay in ms before firing onComplete |
|
|
274
|
+
|
|
275
|
+
**Return value:**
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
{
|
|
279
|
+
status: 'idle' | 'loading' | 'success' | 'canceled' | 'error';
|
|
280
|
+
session: CreemCheckoutSession | null;
|
|
281
|
+
error: CreemError | null;
|
|
282
|
+
startCheckout: () => Promise<void>;
|
|
283
|
+
reset: () => void;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `useCreemSubscription(subscriptionId, options?)`
|
|
288
|
+
|
|
289
|
+
| Option | Type | Description |
|
|
290
|
+
|---|---|---|
|
|
291
|
+
| `pollInterval` | `number` | Re-fetch every N ms. `0` = no polling |
|
|
292
|
+
| `onStatusChange` | `(status) => void` | Fires when the status changes |
|
|
293
|
+
| `enabled` | `boolean` | Enable/disable the fetch. Default: `true` |
|
|
294
|
+
|
|
295
|
+
**Return value:**
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
{
|
|
299
|
+
subscription: CreemSubscription | null;
|
|
300
|
+
status: SubscriptionStatus | null;
|
|
301
|
+
isLoading: boolean;
|
|
302
|
+
error: CreemError | null;
|
|
303
|
+
lastUpdated: Date | null;
|
|
304
|
+
refetch: () => Promise<void>;
|
|
305
|
+
cancelSubscription: (options?) => Promise<void>;
|
|
306
|
+
updateSubscription: (options) => Promise<void>;
|
|
307
|
+
upgradeSubscription: (options) => Promise<void>;
|
|
308
|
+
pauseSubscription: () => Promise<void>;
|
|
309
|
+
resumeSubscription: () => Promise<void>;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### `useCreemProducts(options?)`
|
|
314
|
+
|
|
315
|
+
| Option | Type | Description |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `page` | `number` | Page number. Default: `1` |
|
|
318
|
+
| `pageSize` | `number` | Items per page. Default: `10` |
|
|
319
|
+
| `enabled` | `boolean` | Enable/disable fetching. Default: `true` |
|
|
320
|
+
| `pollInterval` | `number` | Poll interval in ms. Default: `0` |
|
|
321
|
+
|
|
322
|
+
**Return value:**
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
{
|
|
326
|
+
products: CreemProduct[];
|
|
327
|
+
total: number;
|
|
328
|
+
page: number;
|
|
329
|
+
hasMore: boolean;
|
|
330
|
+
isLoading: boolean;
|
|
331
|
+
error: CreemError | null;
|
|
332
|
+
refetch: () => Promise<void>;
|
|
333
|
+
loadMore: () => Promise<void>;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### `useCreemLicense()`
|
|
338
|
+
|
|
339
|
+
**Return value:**
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
{
|
|
343
|
+
license: CreemLicenseKey | null;
|
|
344
|
+
instance: CreemLicenseInstance | null;
|
|
345
|
+
status: 'active' | 'inactive' | 'expired' | 'disabled' | null;
|
|
346
|
+
isLoading: boolean;
|
|
347
|
+
error: CreemError | null;
|
|
348
|
+
activate: (options: CreemActivateLicenseOptions) => Promise<void>;
|
|
349
|
+
validate: (options: CreemValidateLicenseOptions) => Promise<void>;
|
|
350
|
+
deactivate: (options: CreemDeactivateLicenseOptions) => Promise<void>;
|
|
351
|
+
reset: () => void;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### `useCreemCustomerPortal(customerId)`
|
|
356
|
+
|
|
357
|
+
**Return value:**
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
{
|
|
361
|
+
portalUrl: string | null;
|
|
362
|
+
isLoading: boolean;
|
|
363
|
+
error: CreemError | null;
|
|
364
|
+
openPortal: () => Promise<void>;
|
|
365
|
+
generatePortalUrl: () => Promise<string | null>;
|
|
366
|
+
reset: () => void;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Components
|
|
371
|
+
|
|
372
|
+
#### `<CreemProvider>`
|
|
373
|
+
|
|
374
|
+
| Prop | Type | Default | Description |
|
|
375
|
+
|---|---|---|---|
|
|
376
|
+
| `apiKey` | `string` | **required** | Your Creem API key |
|
|
377
|
+
| `environment` | `'production' \| 'sandbox'` | `'production'` | API environment |
|
|
378
|
+
| `baseUrl` | `string` | — | Override base URL |
|
|
379
|
+
| `retries` | `number` | `2` | Retry count |
|
|
380
|
+
| `retryDelay` | `number` | `300` | Retry backoff base (ms) |
|
|
381
|
+
|
|
382
|
+
#### `<CreemCheckoutButton>`
|
|
383
|
+
|
|
384
|
+
| Prop | Type | Default | Description |
|
|
385
|
+
|---|---|---|---|
|
|
386
|
+
| `options` | `UseCreemCheckoutOptions` | **required** | Checkout options |
|
|
387
|
+
| `title` | `string` | `'Subscribe'` | Button label |
|
|
388
|
+
| `loadingTitle` | `string` | — | Label shown while loading |
|
|
389
|
+
| `disabled` | `boolean` | `false` | Disable the button |
|
|
390
|
+
| `variant` | `'primary' \| 'secondary' \| 'outline'` | `'primary'` | Visual style |
|
|
391
|
+
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Button size |
|
|
392
|
+
| `style` | `ViewStyle` | — | Custom container style |
|
|
393
|
+
| `textStyle` | `TextStyle` | — | Custom text style |
|
|
394
|
+
|
|
395
|
+
#### `<SubscriptionStatus>`
|
|
396
|
+
|
|
397
|
+
| Prop | Type | Default | Description |
|
|
398
|
+
|---|---|---|---|
|
|
399
|
+
| `subscriptionId` | `string \| null` | **required** | Subscription ID |
|
|
400
|
+
| `pollInterval` | `number` | `0` | Poll interval (ms) |
|
|
401
|
+
| `showDetails` | `boolean` | `false` | Show renewal/cancel dates |
|
|
402
|
+
| `onStatusChange` | `(status) => void` | — | Status change callback |
|
|
403
|
+
| `renderLoading` | `() => ReactNode` | — | Custom loading renderer |
|
|
404
|
+
| `renderError` | `(error) => ReactNode` | — | Custom error renderer |
|
|
405
|
+
| `renderInactive` | `() => ReactNode` | — | Custom inactive renderer |
|
|
406
|
+
| `renderActive` | `(sub) => ReactNode` | — | Custom active renderer |
|
|
407
|
+
| `renderTrialing` | `(sub) => ReactNode` | — | Custom trial renderer |
|
|
408
|
+
| `renderCanceling` | `(sub) => ReactNode` | — | Custom canceling renderer |
|
|
409
|
+
| `renderPaused` | `(sub) => ReactNode` | — | Custom paused renderer |
|
|
410
|
+
|
|
411
|
+
#### `<SubscriptionBadge>`
|
|
412
|
+
|
|
413
|
+
| Prop | Type | Default | Description |
|
|
414
|
+
|---|---|---|---|
|
|
415
|
+
| `subscriptionId` | `string \| null` | **required** | Subscription ID |
|
|
416
|
+
| `pollInterval` | `number` | `0` | Poll interval (ms) |
|
|
417
|
+
| `showLabel` | `boolean` | `true` | Show text label |
|
|
418
|
+
|
|
419
|
+
### Types
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
type CheckoutStatus = 'pending' | 'processing' | 'completed' | 'expired';
|
|
423
|
+
type SubscriptionStatus = 'active' | 'canceled' | 'unpaid' | 'past_due' | 'paused' | 'trialing' | 'scheduled_cancel';
|
|
424
|
+
type LicenseStatus = 'active' | 'inactive' | 'expired' | 'disabled';
|
|
425
|
+
type BillingType = 'one_time' | 'recurring';
|
|
426
|
+
type BillingPeriod = 'day' | 'week' | 'month' | 'year';
|
|
427
|
+
type UpdateBehavior = 'proration-charge-immediately' | 'proration-charge' | 'proration-none';
|
|
428
|
+
|
|
429
|
+
interface CreemCancelSubscriptionOptions {
|
|
430
|
+
mode?: 'immediate' | 'scheduled';
|
|
431
|
+
onExecute?: 'cancel' | 'pause';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
interface CreemUpdateSubscriptionOptions {
|
|
435
|
+
items: Array<{ id: string; units: number }>;
|
|
436
|
+
update_behavior?: UpdateBehavior;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
interface CreemUpgradeSubscriptionOptions {
|
|
440
|
+
product_id: string;
|
|
441
|
+
update_behavior?: UpdateBehavior;
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Server-side Usage
|
|
448
|
+
|
|
449
|
+
Use `CreemServerClient` in your Node.js / Edge backend to create checkout sessions without exposing your API key to clients.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { CreemServerClient } from 'expo-creem/server';
|
|
453
|
+
|
|
454
|
+
const creem = new CreemServerClient({ apiKey: process.env.CREEM_API_KEY! });
|
|
455
|
+
// or for the test environment:
|
|
456
|
+
const creem = CreemServerClient.sandbox({ apiKey: process.env.CREEM_API_KEY! });
|
|
457
|
+
|
|
458
|
+
// Create a checkout session
|
|
459
|
+
const session = await creem.createCheckoutSession({
|
|
460
|
+
product_id: 'prod_xxx',
|
|
461
|
+
customer: { email: 'user@example.com' },
|
|
462
|
+
success_url: 'https://yourapp.com/success',
|
|
463
|
+
metadata: { userId: '42' },
|
|
464
|
+
});
|
|
465
|
+
console.log(session.checkout_url); // redirect the user here
|
|
466
|
+
|
|
467
|
+
// Full API coverage:
|
|
468
|
+
await creem.getSubscription('sub_xxx');
|
|
469
|
+
await creem.cancelSubscription('sub_xxx', { mode: 'scheduled' });
|
|
470
|
+
await creem.updateSubscription('sub_xxx', { items: [...] });
|
|
471
|
+
await creem.upgradeSubscription('sub_xxx', { product_id: 'prod_new' });
|
|
472
|
+
await creem.pauseSubscription('sub_xxx');
|
|
473
|
+
await creem.resumeSubscription('sub_xxx');
|
|
474
|
+
await creem.getProduct('prod_xxx');
|
|
475
|
+
await creem.searchProducts(1, 10);
|
|
476
|
+
await creem.createProduct({ name: 'Pro', price: 1999, currency: 'USD', billing_type: 'recurring' });
|
|
477
|
+
await creem.getCustomer('cust_xxx');
|
|
478
|
+
await creem.generateCustomerPortalLink('cust_xxx');
|
|
479
|
+
await creem.activateLicense({ key: 'xxx', instance_name: 'server-1' });
|
|
480
|
+
await creem.validateLicense({ key: 'xxx', instance_id: 'inst_xxx' });
|
|
481
|
+
await creem.deactivateLicense({ key: 'xxx', instance_id: 'inst_xxx' });
|
|
482
|
+
await creem.getDiscount('disc_xxx');
|
|
483
|
+
await creem.getDiscountByCode('SUMMER2024');
|
|
484
|
+
await creem.createDiscount({ name: 'Sale', code: 'SALE20', type: 'percentage', percentage: 20 });
|
|
485
|
+
await creem.deleteDiscount('disc_xxx');
|
|
486
|
+
await creem.getTransaction('txn_xxx');
|
|
487
|
+
await creem.searchTransactions('cust_xxx');
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Webhook verification
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { validateWebhookSignature, parseWebhookEvent, processWebhookEvent } from 'expo-creem/server';
|
|
494
|
+
|
|
495
|
+
// Express example
|
|
496
|
+
app.post('/webhooks/creem', express.raw({ type: 'application/json' }), (req, res) => {
|
|
497
|
+
const signature = req.headers['creem-signature'] as string;
|
|
498
|
+
const payload = req.body.toString();
|
|
499
|
+
|
|
500
|
+
if (!validateWebhookSignature(payload, signature, process.env.CREEM_WEBHOOK_SECRET!)) {
|
|
501
|
+
return res.status(400).send('Invalid signature');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const event = parseWebhookEvent(payload);
|
|
505
|
+
// handle the event ...
|
|
506
|
+
res.status(200).send('OK');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Or use the typed handler system:
|
|
510
|
+
await processWebhookEvent(event, {
|
|
511
|
+
'checkout.completed': async (data) => {
|
|
512
|
+
console.log('Payment completed:', data);
|
|
513
|
+
},
|
|
514
|
+
'subscription.active': async (data) => {
|
|
515
|
+
console.log('Subscription activated:', data);
|
|
516
|
+
},
|
|
517
|
+
'subscription.canceled': async (data) => {
|
|
518
|
+
console.log('Subscription canceled:', data);
|
|
519
|
+
},
|
|
520
|
+
}, (event) => {
|
|
521
|
+
console.log('Unhandled event:', event.eventType);
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Environment Variables
|
|
528
|
+
|
|
529
|
+
```env
|
|
530
|
+
# Client (prefix with EXPO_PUBLIC_ to expose to the RN bundle)
|
|
531
|
+
EXPO_PUBLIC_CREEM_API_KEY=your_api_key
|
|
532
|
+
EXPO_PUBLIC_PRODUCT_ID=prod_xxx
|
|
533
|
+
|
|
534
|
+
# Server only
|
|
535
|
+
CREEM_API_KEY=your_api_key
|
|
536
|
+
CREEM_WEBHOOK_SECRET=your_webhook_secret
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Example App
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
cd example
|
|
545
|
+
npm install
|
|
546
|
+
npx expo start
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Architecture
|
|
552
|
+
|
|
553
|
+
```
|
|
554
|
+
expo-creem/
|
|
555
|
+
├── src/
|
|
556
|
+
│ ├── types/ # All TypeScript definitions (25+ interfaces)
|
|
557
|
+
│ ├── hooks/ # React hooks (6 hooks)
|
|
558
|
+
│ │ ├── useCreemCheckout.ts
|
|
559
|
+
│ │ ├── useCreemSubscription.ts
|
|
560
|
+
│ │ ├── useCreemProducts.ts
|
|
561
|
+
│ │ ├── useCreemLicense.ts
|
|
562
|
+
│ │ └── useCreemCustomerPortal.ts
|
|
563
|
+
│ ├── components/ # UI components
|
|
564
|
+
│ │ ├── CreemCheckoutButton.tsx
|
|
565
|
+
│ │ └── SubscriptionStatus.tsx
|
|
566
|
+
│ ├── utils/ # Client + helpers
|
|
567
|
+
│ │ ├── client.ts # CreemClient with retry logic
|
|
568
|
+
│ │ └── context.tsx # Provider + formatting helpers
|
|
569
|
+
│ ├── server/ # Server-side helpers
|
|
570
|
+
│ │ └── creem-server.ts
|
|
571
|
+
│ ├── launcher.ts # Browser launch logic
|
|
572
|
+
│ └── index.ts # Barrel exports
|
|
573
|
+
├── plugin/
|
|
574
|
+
│ └── index.js # Expo config plugin (pure JS)
|
|
575
|
+
├── example/
|
|
576
|
+
│ └── App.tsx # Full demo app
|
|
577
|
+
├── app.plugin.js # Plugin entry point
|
|
578
|
+
└── package.json
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Platform Support
|
|
584
|
+
|
|
585
|
+
| Platform | Support |
|
|
586
|
+
|---|---|
|
|
587
|
+
| iOS | Full (SFSafariViewController via expo-web-browser) |
|
|
588
|
+
| Android | Full (Chrome Custom Tabs via expo-web-browser) |
|
|
589
|
+
| Web | Full (redirect flow) |
|
|
590
|
+
|
|
591
|
+
## License
|
|
592
|
+
|
|
593
|
+
MIT
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/index');
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-creem-integration",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Creem payment integration for Expo apps — checkout sessions, subscriptions, licenses, customer portal, and deep-link handling. Zero native code required.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"expo": {
|
|
8
|
+
"plugin": "./plugin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.ts",
|
|
12
|
+
"./server": "./src/server/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"expo",
|
|
20
|
+
"creem",
|
|
21
|
+
"payments",
|
|
22
|
+
"checkout",
|
|
23
|
+
"subscriptions",
|
|
24
|
+
"license",
|
|
25
|
+
"in-app-purchases",
|
|
26
|
+
"react-native",
|
|
27
|
+
"merchant-of-record",
|
|
28
|
+
"saas"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"plugin",
|
|
35
|
+
"app.plugin.js",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@expo/config-plugins": "*",
|
|
41
|
+
"expo": "*",
|
|
42
|
+
"expo-linking": "*",
|
|
43
|
+
"expo-web-browser": "*",
|
|
44
|
+
"react": "*",
|
|
45
|
+
"react-native": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@expo/config-plugins": "^8.0.0",
|
|
49
|
+
"@types/react": "^18.2.0",
|
|
50
|
+
"@types/react-native": "^0.73.0",
|
|
51
|
+
"typescript": "^5.3.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"expo-linking": "~7.0.0",
|
|
55
|
+
"expo-web-browser": "~14.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|