Skip to content

Commit f36c604

Browse files
authored
feat(ios): add getAppTransaction support for iOS 16.0+ (#101)
- Add AppTransaction TypeScript type definition - Implement native getAppTransaction method in Swift module - Export getAppTransaction function from ios module - Add comprehensive documentation for the new API - Fix appTransactionID field name to match Apple's API Port of [react-native-iap PR #2969](hyochan/react-native-iap#2969)
1 parent f9142f9 commit f36c604

File tree

5 files changed

+86
-1
lines changed

5 files changed

+86
-1
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,46 @@ const fetchStorefront = async () => {
115115

116116
**Note:** This is useful for region-specific pricing, content, or features.
117117

118+
## getAppTransaction()
119+
120+
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.
121+
122+
```tsx
123+
import {getAppTransaction} from 'expo-iap';
124+
125+
const fetchAppTransaction = async () => {
126+
try {
127+
const appTransaction = await getAppTransaction();
128+
if (appTransaction) {
129+
console.log('App Transaction ID:', appTransaction.appTransactionID);
130+
console.log('Original Purchase Date:', new Date(appTransaction.originalPurchaseDate));
131+
console.log('Device Verification:', appTransaction.deviceVerification);
132+
} else {
133+
console.log('No app transaction found (app may be free)');
134+
}
135+
} catch (error) {
136+
console.error('Failed to get app transaction:', error);
137+
}
138+
};
139+
```
140+
141+
**Returns:** `Promise<AppTransactionIOS | null>` - Returns the app transaction information or null if not available.
142+
143+
**Platform:** iOS 16.0+ only
144+
145+
**AppTransactionIOS Interface:**
146+
```typescript
147+
interface AppTransactionIOS {
148+
appTransactionID: string;
149+
originalAppAccountToken?: string;
150+
originalPurchaseDate: number; // milliseconds since epoch
151+
deviceVerification: string;
152+
deviceVerificationNonce: string;
153+
}
154+
```
155+
156+
**Note:** This is useful for verifying that a user legitimately purchased your app. The device verification data can be sent to your server for validation.
157+
118158
## getProducts()
119159

120160
Fetches product information from the store.

ios/ExpoIapModule.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,28 @@ public class ExpoIapModule: Module {
255255
return storefront?.countryCode
256256
}
257257

258+
AsyncFunction("getAppTransaction") { () async throws -> [String: Any?]? in
259+
if #available(iOS 16.0, *) {
260+
guard let appTransaction = try await AppTransaction.shared else {
261+
return nil
262+
}
263+
264+
return [
265+
"appTransactionID": appTransaction.appAppleId,
266+
"originalAppAccountToken": appTransaction.originalAppAccountToken,
267+
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
268+
"deviceVerification": appTransaction.deviceVerification.base64EncodedString(),
269+
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString
270+
]
271+
} else {
272+
throw Exception(
273+
name: "ExpoIapModule",
274+
description: "getAppTransaction requires iOS 16.0 or later",
275+
code: IapErrorCode.featureNotSupported
276+
)
277+
}
278+
}
279+
258280
AsyncFunction("getItems") { (skus: [String]) -> [[String: Any?]?] in
259281
guard let productStore = self.productStore else {
260282
throw Exception(name: "ExpoIapModule", description: "Connection not initialized", code: IapErrorCode.notPrepared)

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {isProductAndroid} from './modules/android';
2929
export * from './ExpoIap.types';
3030
export * from './modules/android';
3131
export * from './modules/ios';
32+
export type {AppTransactionIOS} from './types/ExpoIapIos.types';
3233

3334
// Get the native constant value
3435
export const PI = ExpoIapModule.PI;

src/modules/ios.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
Purchase,
77
SubscriptionPurchase,
88
} from '../ExpoIap.types';
9-
import type {ProductStatusIos} from '../types/ExpoIapIos.types';
9+
import type {ProductStatusIos, AppTransactionIOS} from '../types/ExpoIapIos.types';
1010
import ExpoIapModule from '../ExpoIapModule';
1111

1212
export type TransactionEvent = {
@@ -220,3 +220,17 @@ export const presentCodeRedemptionSheet = (): Promise<boolean> => {
220220
}
221221
return ExpoIapModule.presentCodeRedemptionSheet();
222222
};
223+
224+
/**
225+
* Get app transaction information (iOS 16.0+).
226+
* AppTransaction represents the initial purchase that unlocked the app.
227+
*
228+
* @returns {Promise<AppTransactionIOS | null>} The app transaction information or null if not available
229+
* @throws {Error} If called on non-iOS platform or iOS version < 16.0
230+
*/
231+
export const getAppTransaction = (): Promise<AppTransactionIOS | null> => {
232+
if (Platform.OS !== 'ios') {
233+
throw new Error('This method is only available on iOS');
234+
}
235+
return ExpoIapModule.getAppTransaction();
236+
};

src/types/ExpoIapIos.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,11 @@ export type ProductPurchaseIos = PurchaseBase & {
140140
currencyIos?: string;
141141
jwsRepresentationIos?: string;
142142
};
143+
144+
export type AppTransactionIOS = {
145+
appTransactionID: string;
146+
originalAppAccountToken?: string;
147+
originalPurchaseDate: number;
148+
deviceVerification: string;
149+
deviceVerificationNonce: string;
150+
};

0 commit comments

Comments
 (0)