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 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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![Expo SDK](https://img.shields.io/badge/Expo%20SDK-52%2B-blue)](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
+ }