Skip to content

Commit bca1999

Browse files
authored
Refactor Core APIs - Platform-Specific Request Structure (#109)
## Summary This PR introduces a cleaner, more intuitive API for handling platform-specific purchase requests in expo-iap v2.7.0. The new API eliminates the need for `Platform.OS` checks in userland code by providing explicit platform parameters. ## Motivation Previously, developers had to write conditional logic to handle platform differences: ```tsx // Old approach - requires platform checks if (Platform.OS === 'ios') { await requestPurchase({ request: { sku: productId } }); } else { await requestPurchase({ request: { skus: [productId] } }); } ``` This approach had several issues: - Required manual platform checks - Easy to miss platform-specific parameters - TypeScript couldn't provide proper platform-specific type hints ## Changes ### 1. New Platform-Specific API ```tsx // New approach - clear platform separation await requestPurchase({ request: { ios: { sku: productId, appAccountToken: 'user-123', }, android: { skus: [productId], obfuscatedAccountIdAndroid: 'user-123', } } }); ``` ### 2. New requestProducts API - Added `requestProducts` to replace deprecated `getProducts` and `getSubscriptions` - Supports both platform-specific and unified parameter structure - When parameters are identical, can use simplified API: ```tsx // Simplified API when parameters are the same await requestProducts({ skus: ['product1', 'product2'], type: 'inapp' }); ``` ### 3. requestSubscription Deprecation - `requestSubscription` is now deprecated - Use `requestPurchase({ type: 'subs' })` instead - Unifies the API for both products and subscriptions ### 4. Platform-Specific Method Improvements - Changed platform-specific methods to show warnings instead of throwing errors - Methods like `getStorefrontIOS()` now return empty values with console warnings on Android - Prevents app crashes when accidentally calling wrong platform methods ### 5. Type System Improvements - Separated modern and legacy request types for future migration - Added discriminated unions for better type safety - Improved TypeScript autocompletion ### 6. Documentation Updates - Added versioning support (2.6 and 2.7) - Updated all examples to use new API - Added comprehensive migration guide - Fixed anti-pattern of using useEffect with currentPurchase ### 7. Google Play Billing Library v8.0.0 - Updated to latest Google Play Billing Library - Removed deprecated getPurchaseHistory method on Android - Improved error handling and type safety ## Benefits - ✅ **Better Type Safety**: TypeScript provides accurate autocompletion - ✅ **Cleaner Code**: No more Platform.OS checks in purchase logic - ✅ **Backward Compatible**: Old API still works for gradual migration - ✅ **Unified API**: Same function for both products and subscriptions - ✅ **Safer Cross-Platform**: Platform methods warn instead of crash ## Migration The old API continues to work, allowing gradual migration: ```tsx // Both approaches work in v2.7.0 // Old way (still supported) if (Platform.OS === 'ios') { await requestPurchase({ request: { sku: productId } }); } // New way (recommended) await requestPurchase({ request: { ios: { sku: productId }, android: { skus: [productId] } } }); ``` ## Testing - [x] Tested on iOS device - [x] Tested on Android device - [x] Updated unit tests - [x] Documentation builds successfully - [x] Examples run without errors - [x] TypeScript compilation passes - [x] Lint checks pass ## Related Issues Addresses feedback from the community about API complexity and platform handling. ## Commits 1. `feat(types)`: Add platform-specific request types for v2.7.0 2. `feat(api)`: Implement new platform-specific requestPurchase API 3. `feat(api)`: Add requestProducts API and improve platform-specific error handling 4. `refactor(ios)`: Remove redundant Platform.OS checks 5. `refactor(android)`: Update to Google Play Billing Library v8.0.0 6. `refactor(types)`: Separate modern and legacy request types 7. `refactor(api)`: Simplify requestProducts API for unified parameters 8. `refactor(examples)`: Update examples to use new platform-specific API 9. `docs`: Update documentation for v2.7.0 platform-specific API 10. `docs`: Setup documentation versioning for v2.6 and v2.7 11. `docs(blog)`: Add comprehensive v2.7.0 release notes 12. `fix`: Update all documentation to avoid currentPurchase anti-pattern
1 parent 9336a44 commit bca1999

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+9930
-452
lines changed

.vscode/launch.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,29 @@
6969
"runtimeExecutable": "pod",
7070
"runtimeArgs": ["install"],
7171
"console": "integratedTerminal"
72+
},
73+
{
74+
"name": "Start Documentation",
75+
"type": "node",
76+
"request": "launch",
77+
"cwd": "${workspaceFolder}/docs",
78+
"runtimeExecutable": "npm",
79+
"runtimeArgs": ["run", "start"],
80+
"console": "integratedTerminal",
81+
"serverReadyAction": {
82+
"pattern": "Local site started at (https?://localhost:[0-9]+)",
83+
"uriFormat": "%s",
84+
"action": "openExternally"
85+
}
86+
},
87+
{
88+
"name": "Build Documentation",
89+
"type": "node",
90+
"request": "launch",
91+
"cwd": "${workspaceFolder}/docs",
92+
"runtimeExecutable": "npm",
93+
"runtimeArgs": ["run", "build"],
94+
"console": "integratedTerminal"
7295
}
7396
],
7497
"compounds": [

docs/CONVENTIONS.md

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,25 @@ Functions that handle platform differences internally do NOT need suffixes.
9898
#### ✅ Correct: Platform-Specific Functions
9999

100100
```typescript
101-
// iOS-only functions
101+
// iOS-only functions (no Platform.OS check needed in iOS module)
102102
export const validateReceiptIOS = async (sku: string): Promise<boolean> => {
103-
if (Platform.OS !== 'ios') {
104-
throw new Error('This method is only available on iOS');
105-
}
106103
// iOS implementation
104+
return ExpoIapModule.validateReceiptIOS(sku);
107105
};
108106

109107
export const getStorefrontIOS = async (): Promise<string> => {
110-
if (Platform.OS !== 'ios') {
111-
throw new Error('This method is only available on iOS');
112-
}
113108
return ExpoIapModule.getStorefront();
114109
};
115110

116-
// Android-only functions
111+
// Android-only functions (no Platform.OS check needed in Android module)
117112
export const deepLinkToSubscriptionsAndroid = async (sku?: string): Promise<void> => {
118-
if (Platform.OS !== 'android') {
119-
throw new Error('This method is only available on Android');
120-
}
121113
// Android implementation
114+
return ExpoIapModule.deepLinkToSubscriptions(sku);
122115
};
123116
```
124117

118+
**Note**: Platform-specific modules (ios.ts, android.ts) don't need Platform.OS checks. The main API (index.ts) handles platform routing.
119+
125120
#### ✅ Correct: Cross-Platform Functions
126121

127122
```typescript
@@ -133,8 +128,18 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
133128
})() || [];
134129
};
135130

136-
export const requestPurchase = async (sku: string): Promise<Purchase> => {
137-
// Handles both platforms internally
131+
// New v2.7.0+ API - No Platform.OS checks needed!
132+
export const requestPurchase = async (productId: string): Promise<Purchase> => {
133+
return requestPurchase({
134+
request: {
135+
ios: {
136+
sku: productId,
137+
},
138+
android: {
139+
skus: [productId],
140+
}
141+
}
142+
});
138143
};
139144
```
140145

@@ -192,35 +197,40 @@ const maxretrycount = 3; // Wrong case
192197
Always use descriptive error messages and proper error types:
193198

194199
```typescript
195-
// ✅ Good
196-
if (Platform.OS !== 'ios') {
197-
throw new Error('getStorefrontIOS: This method is only available on iOS');
200+
// ✅ Good - Clear error message
201+
try {
202+
await requestPurchase({ request: { ios: { sku: 'invalid' } } });
203+
} catch (error) {
204+
if (error.code === 'E_ITEM_UNAVAILABLE') {
205+
console.error('Product not found in store');
206+
}
198207
}
199208

200-
// ❌ Bad
201-
if (Platform.OS !== 'ios') {
202-
throw new Error('Not supported');
209+
// ❌ Bad - Generic error handling
210+
try {
211+
await requestPurchase({ request: { sku: 'invalid' } });
212+
} catch (error) {
213+
console.error('Error');
203214
}
204215
```
205216

206-
When using platform-specific functions, always handle unsupported platforms:
217+
When using platform-specific functions, handle errors gracefully:
207218

208219
```typescript
209-
// ✅ Good - Show warning for unsupported platforms
210-
if (Platform.OS === 'ios') {
211-
getStorefrontIOS().then((storefront) => {
220+
// ✅ Good - Let the function handle platform checks internally
221+
getStorefrontIOS()
222+
.then((storefront) => {
212223
console.log('Storefront:', storefront);
213-
}).catch((error) => {
214-
console.warn('Failed to get storefront:', error.message);
224+
})
225+
.catch((error) => {
226+
// Will throw on non-iOS platforms
227+
console.log('Storefront not available:', error.message);
215228
});
216-
} else {
217-
console.warn('getStorefrontIOS is not supported on this platform');
218-
}
219229

220-
// ❌ Bad - Silently ignore errors
230+
// ❌ Bad - Redundant platform check
221231
if (Platform.OS === 'ios') {
222-
getStorefrontIOS().catch(() => {
223-
// Ignore error on non-iOS platforms
232+
getStorefrontIOS().then((storefront) => {
233+
console.log('Storefront:', storefront);
224234
});
225235
}
226236
```
@@ -388,9 +398,6 @@ Example:
388398
```typescript
389399
// Step 1: Add new function with correct naming
390400
export const getStorefrontIOS = async (): Promise<string> => {
391-
if (Platform.OS !== 'ios') {
392-
throw new Error('This method is only available on iOS');
393-
}
394401
return ExpoIapModule.getStorefront();
395402
};
396403

@@ -404,6 +411,69 @@ export const getStorefront = async (): Promise<string> => {
404411
};
405412
```
406413

414+
## New v2.7.0 API Guidelines
415+
416+
### Platform-Specific Request Structure
417+
418+
Use the new platform-specific API to avoid Platform.OS checks:
419+
420+
```typescript
421+
// ✅ Good - New v2.7.0 API
422+
await requestPurchase({
423+
request: {
424+
ios: {
425+
sku: productId,
426+
quantity: 1,
427+
appAccountToken: 'user-123',
428+
},
429+
android: {
430+
skus: [productId],
431+
obfuscatedAccountIdAndroid: 'user-123',
432+
}
433+
},
434+
type: 'inapp'
435+
});
436+
437+
// ❌ Bad - Old API with Platform.OS checks
438+
if (Platform.OS === 'ios') {
439+
await requestPurchase({
440+
request: { sku: productId },
441+
});
442+
} else {
443+
await requestPurchase({
444+
request: { skus: [productId] },
445+
});
446+
}
447+
```
448+
449+
### Subscription Purchases
450+
451+
```typescript
452+
// ✅ Good - Unified subscription API
453+
await requestPurchase({
454+
request: {
455+
ios: {
456+
sku: subscriptionId,
457+
appAccountToken: 'user-123',
458+
},
459+
android: {
460+
skus: [subscriptionId],
461+
subscriptionOffers: [{
462+
sku: subscriptionId,
463+
offerToken: offer.offerToken,
464+
}],
465+
}
466+
},
467+
type: 'subs'
468+
});
469+
470+
// ❌ Bad - Using deprecated requestSubscription
471+
await requestSubscription({
472+
sku: subscriptionId,
473+
skus: [subscriptionId],
474+
});
475+
```
476+
407477
## Benefits
408478

409479
Following these conventions provides:
@@ -413,4 +483,5 @@ Following these conventions provides:
413483
3. **Type Safety**: Better TypeScript inference and error prevention
414484
4. **Documentation**: Self-documenting code
415485
5. **Platform Safety**: Prevents platform-specific bugs
416-
6. **Developer Experience**: Easier onboarding and collaboration
486+
6. **Developer Experience**: Easier onboarding and collaboration
487+
7. **No Platform Checks**: New API eliminates Platform.OS branching

0 commit comments

Comments
 (0)