Skip to content

Commit 667697f

Browse files
authored
feat: add iOS 16.0+ app transaction support with SDK version check (#113)
## Summary - Added getAppTransactionIOS method for iOS 16.0+ devices - Implemented compiler version check to ensure Xcode 15.0+ with iOS 16.0 SDK - Added comprehensive documentation and test UI in example app ## Changes - **iOS Module**: Added #if compiler(>=5.7) check to ensure proper SDK version - **TypeScript**: Updated method documentation with requirements - **Docs**: Added warning box about runtime and build requirements - **Example**: Added test button to verify getAppTransaction functionality ## Test plan - [ ] Build with Xcode 15.0+ - verify compilation succeeds - [ ] Build with older Xcode - verify appropriate error message - [ ] Test on iOS 16.0+ device - verify getAppTransaction works - [ ] Test on iOS < 16.0 - verify appropriate runtime error - [ ] Test example app button functionality
1 parent 9c593d0 commit 667697f

File tree

5 files changed

+144
-3
lines changed

5 files changed

+144
-3
lines changed

docs/docs/api/methods/core-methods.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ const fetchStorefront = async () => {
130130

131131
Gets app transaction information for iOS apps (iOS 16.0+). AppTransaction represents the initial purchase that unlocked the app, useful for premium apps or apps that were previously paid.
132132

133+
> **⚠️ Important Requirements:**
134+
> - **Runtime:** iOS 16.0 or later
135+
> - **Build Environment:** Xcode 15.0+ with iOS 16.0 SDK
136+
> - If built with older SDK versions, the method will throw an error
137+
133138
```tsx
134139
import {getAppTransaction} from 'expo-iap';
135140

example/__tests__/ios-functions.test.tsx

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,98 @@ describe('iOS Functions Tests', () => {
4141
});
4242
});
4343

44+
describe('getAppTransactionIOS Tests', () => {
45+
beforeEach(() => {
46+
jest.clearAllMocks();
47+
});
48+
49+
it('should call getAppTransactionIOS successfully', async () => {
50+
const mockAppTransaction: ExpoIap.AppTransactionIOS = {
51+
appTransactionID: 'test-transaction-id',
52+
bundleID: 'com.example.testapp',
53+
appVersion: '2.0.0',
54+
originalAppVersion: '1.0.0',
55+
originalPurchaseDate: 1234567890000,
56+
deviceVerification: 'device-verification-data',
57+
deviceVerificationNonce: 'nonce-value',
58+
environment: 'Production',
59+
signedDate: 1234567900000,
60+
appID: 123456789,
61+
appVersionID: 987654321,
62+
originalPlatform: 'iOS',
63+
preorderDate: undefined,
64+
};
65+
66+
(ExpoIap.getAppTransactionIOS as jest.Mock).mockResolvedValue(
67+
mockAppTransaction,
68+
);
69+
70+
const result = await ExpoIap.getAppTransactionIOS();
71+
72+
expect(ExpoIap.getAppTransactionIOS).toHaveBeenCalledTimes(1);
73+
expect(result).toEqual(mockAppTransaction);
74+
expect(result?.appTransactionID).toBe('test-transaction-id');
75+
expect(result?.environment).toBe('Production');
76+
});
77+
78+
it('should handle null response from getAppTransactionIOS', async () => {
79+
(ExpoIap.getAppTransactionIOS as jest.Mock).mockResolvedValue(null);
80+
81+
const result = await ExpoIap.getAppTransactionIOS();
82+
83+
expect(ExpoIap.getAppTransactionIOS).toHaveBeenCalledTimes(1);
84+
expect(result).toBeNull();
85+
});
86+
87+
it('should handle errors from getAppTransactionIOS', async () => {
88+
const mockError = new Error('iOS 16.0+ required');
89+
(ExpoIap.getAppTransactionIOS as jest.Mock).mockRejectedValue(mockError);
90+
91+
await expect(ExpoIap.getAppTransactionIOS()).rejects.toThrow(
92+
'iOS 16.0+ required',
93+
);
94+
expect(ExpoIap.getAppTransactionIOS).toHaveBeenCalledTimes(1);
95+
});
96+
97+
it('should handle SDK version error', async () => {
98+
const sdkError = new Error(
99+
'getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation',
100+
);
101+
(ExpoIap.getAppTransactionIOS as jest.Mock).mockRejectedValue(sdkError);
102+
103+
await expect(ExpoIap.getAppTransactionIOS()).rejects.toThrow(
104+
'getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation',
105+
);
106+
});
107+
108+
it('should handle deprecated getAppTransaction function', async () => {
109+
const mockTransaction: ExpoIap.AppTransactionIOS = {
110+
appTransactionID: 'deprecated-test',
111+
bundleID: 'com.example.app',
112+
appVersion: '1.0.0',
113+
originalAppVersion: '1.0.0',
114+
originalPurchaseDate: Date.now(),
115+
deviceVerification: 'verification',
116+
deviceVerificationNonce: 'nonce',
117+
environment: 'Sandbox',
118+
signedDate: Date.now(),
119+
appID: 123456,
120+
appVersionID: 789012,
121+
originalPlatform: 'iOS',
122+
};
123+
124+
(ExpoIap.getAppTransaction as jest.Mock).mockResolvedValue(
125+
mockTransaction,
126+
);
127+
128+
const result = await ExpoIap.getAppTransaction();
129+
130+
expect(ExpoIap.getAppTransaction).toHaveBeenCalledTimes(1);
131+
expect(result).toEqual(mockTransaction);
132+
expect(result?.environment).toBe('Sandbox');
133+
});
134+
});
135+
44136
describe('Type Exports', () => {
45137
it('should have proper type structure for AppTransactionIOS', () => {
46138
// Type checking through object creation
@@ -95,4 +187,4 @@ describe('iOS Functions Tests', () => {
95187
expect(ExpoIap.presentCodeRedemptionSheet).toBeDefined();
96188
});
97189
});
98-
});
190+
});

example/app/purchase-flow.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ScrollView,
99
Platform,
1010
} from 'react-native';
11-
import {requestPurchase, useIAP} from '../../src';
11+
import {requestPurchase, useIAP, getAppTransactionIOS} from '../../src';
1212
import type {
1313
Product,
1414
ProductPurchase,
@@ -196,6 +196,25 @@ export default function PurchaseFlow() {
196196
{'\n'}• CPK React Native compliance
197197
</Text>
198198
</View>
199+
200+
{Platform.OS === 'ios' && (
201+
<View style={styles.section}>
202+
<Text style={styles.sectionTitle}>Test iOS 16.0 Feature</Text>
203+
<TouchableOpacity
204+
style={styles.testButton}
205+
onPress={async () => {
206+
try {
207+
const appTransaction = await getAppTransactionIOS();
208+
Alert.alert('Success', `App Transaction: ${JSON.stringify(appTransaction)}`);
209+
} catch (error: any) {
210+
Alert.alert('Error', error.message || 'Failed to get app transaction');
211+
}
212+
}}
213+
>
214+
<Text style={styles.testButtonText}>Test getAppTransaction</Text>
215+
</TouchableOpacity>
216+
</View>
217+
)}
199218
</ScrollView>
200219
);
201220
}
@@ -339,4 +358,16 @@ const styles = StyleSheet.create({
339358
color: '#0066cc',
340359
lineHeight: 20,
341360
},
361+
testButton: {
362+
backgroundColor: '#FF6B6B',
363+
paddingHorizontal: 20,
364+
paddingVertical: 12,
365+
borderRadius: 8,
366+
alignItems: 'center',
367+
},
368+
testButtonText: {
369+
color: '#fff',
370+
fontWeight: '600',
371+
fontSize: 16,
372+
},
342373
});

ios/ExpoIapModule.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public class ExpoIapModule: Module {
257257

258258
AsyncFunction("getAppTransaction") { () async throws -> [String: Any?]? in
259259
if #available(iOS 16.0, *) {
260+
#if compiler(>=5.7)
260261
let verificationResult = try await AppTransaction.shared
261262

262263
let appTransaction: AppTransaction
@@ -287,6 +288,13 @@ public class ExpoIapModule: Module {
287288
}
288289

289290
return result
291+
#else
292+
throw Exception(
293+
name: "ExpoIapModule",
294+
description: "getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation",
295+
code: IapErrorCode.unknown
296+
)
297+
#endif
290298
} else {
291299
throw Exception(
292300
name: "ExpoIapModule",

src/modules/ios.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,10 +264,15 @@ export const presentCodeRedemptionSheetIOS = (): Promise<boolean> => {
264264
* Get app transaction information (iOS 16.0+).
265265
* AppTransaction represents the initial purchase that unlocked the app.
266266
*
267+
* NOTE: This function requires:
268+
* - iOS 16.0 or later at runtime
269+
* - Xcode 15.0+ with iOS 16.0 SDK for compilation
270+
*
267271
* @returns Promise resolving to the app transaction information or null if not available
268-
* @throws Error if called on non-iOS platform or iOS version < 16.0
272+
* @throws Error if called on non-iOS platform, iOS version < 16.0, or compiled with older SDK
269273
*
270274
* @platform iOS
275+
* @since iOS 16.0
271276
*/
272277
export const getAppTransactionIOS = (): Promise<AppTransactionIOS | null> => {
273278
return ExpoIapModule.getAppTransaction();

0 commit comments

Comments
 (0)