Skip to content

Commit 9326bbf

Browse files
authored
fix: add missing AppTransaction properties and fix native method name (#107)
## Summary This PR fixes the iOS build errors reported in #106 by adding missing AppTransaction properties and correcting the native method name. https://developer.apple.com/documentation/storekit/apptransaction ## Changes - **Add all missing AppTransaction properties** to match Apple's API documentation: - `appTransactionID` - The unique identifier (this was the main missing property) - `environment` - App Store environment - `signedDate` - When the transaction was signed - `appID` - App Store app ID - `appVersionID` - App Store version ID - `originalPlatform` - Platform of original purchase - `preorderDate` - Pre-order date if applicable - **Fix native method name**: `validateReceiptIos` → `validateReceiptIOS` to match TypeScript calls - **Update TypeScript types**: Added all properties to `AppTransactionIOS` type definition ## Fixes Fixes #106 - iOS build error with missing AppTransaction properties ## Testing - [ ] Build passes on iOS - [ ] AppTransaction returns all properties correctly - [ ] validateReceiptIOS works as expected
1 parent 9cbbe33 commit 9326bbf

14 files changed

+925
-5
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
slug: v2-6-2-release
3+
title: Version 2.6.2 - iOS AppTransaction Support
4+
authors: [hyochan]
5+
tags: [release, ios, apptransaction]
6+
---
7+
8+
# Version 2.6.2 Release: Enhanced iOS AppTransaction Support
9+
10+
We're excited to announce the release of expo-iap version 2.6.2, which includes critical fixes for iOS AppTransaction functionality.
11+
12+
## What's New
13+
14+
### Complete AppTransaction Properties
15+
16+
The `getAppTransactionIOS()` function now returns all properties available from Apple's StoreKit 2 AppTransaction API:
17+
18+
```typescript
19+
export type AppTransactionIOS = {
20+
appTransactionID: string;
21+
bundleID: string;
22+
appVersion: string;
23+
originalAppVersion: string;
24+
originalPurchaseDate: number;
25+
deviceVerification: string;
26+
deviceVerificationNonce: string;
27+
environment: string;
28+
signedDate: number;
29+
appID?: number;
30+
appVersionID?: number;
31+
originalPlatform: string;
32+
preorderDate?: number;
33+
};
34+
```
35+
36+
### Key Properties Added
37+
38+
- **`appTransactionID`**: The unique identifier for the app transaction
39+
- **`environment`**: Indicates whether the transaction occurred in Production, Sandbox, or Xcode environment
40+
- **`signedDate`**: The date when the transaction was signed
41+
- **`appID`** and **`appVersionID`**: App Store identifiers for the app and version
42+
- **`originalPlatform`**: The platform where the app was originally purchased
43+
- **`preorderDate`**: Available when the app was pre-ordered
44+
45+
## Usage Example
46+
47+
```typescript
48+
import { getAppTransactionIOS } from 'expo-iap';
49+
50+
const getAppPurchaseInfo = async () => {
51+
try {
52+
const appTransaction = await getAppTransactionIOS();
53+
54+
console.log('App Transaction ID:', appTransaction.appTransactionID);
55+
console.log('Environment:', appTransaction.environment);
56+
console.log('Original Purchase Date:', new Date(appTransaction.originalPurchaseDate));
57+
58+
// Check if app was pre-ordered
59+
if (appTransaction.preorderDate) {
60+
console.log('Pre-order Date:', new Date(appTransaction.preorderDate));
61+
}
62+
} catch (error) {
63+
console.error('Failed to get app transaction:', error);
64+
}
65+
};
66+
```
67+
68+
## Why This Matters
69+
70+
The AppTransaction API provides crucial information about the app's purchase and installation:
71+
72+
- **Verify app authenticity**: Use the device verification data to ensure the app is legitimate
73+
- **Track installation source**: Determine if the app was purchased, redeemed, or installed via TestFlight
74+
- **Environment detection**: Easily identify if you're running in production or sandbox
75+
- **Pre-order support**: Handle pre-ordered apps appropriately
76+
77+
## Requirements
78+
79+
- iOS 16.0 or later
80+
- The function will throw an error on older iOS versions
81+
82+
## Bug Fixes
83+
84+
This release also includes:
85+
- Fixed missing properties in AppTransaction type definition
86+
- Corrected property names to match Apple's API exactly
87+
- Fixed TypeScript type exports
88+
89+
## Upgrading
90+
91+
To upgrade to version 2.6.2:
92+
93+
```bash
94+
npm install expo-iap@2.6.2
95+
# or
96+
yarn add expo-iap@2.6.2
97+
# or
98+
bun add expo-iap@2.6.2
99+
```
100+
101+
## What's Next
102+
103+
We're continuing to improve the iOS integration and will be adding more StoreKit 2 features in upcoming releases. Stay tuned!
104+
105+
## Feedback
106+
107+
If you encounter any issues or have suggestions, please [open an issue](https://github.com/hyochan/expo-iap/issues) on our GitHub repository.
108+
109+
Happy coding! 🚀
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
describe('AppTransaction Type Tests', () => {
2+
it('should validate AppTransactionIOS type structure', () => {
3+
// This test validates that we have the correct type structure
4+
// It doesn't run actual code but ensures TypeScript compilation
5+
const mockTransaction = {
6+
appTransactionID: 'test-id',
7+
bundleID: 'com.example.app',
8+
appVersion: '1.0.0',
9+
originalAppVersion: '1.0.0',
10+
originalPurchaseDate: Date.now(),
11+
deviceVerification: 'verification-data',
12+
deviceVerificationNonce: 'nonce',
13+
environment: 'Production',
14+
signedDate: Date.now(),
15+
appID: 123456,
16+
appVersionID: 789012,
17+
originalPlatform: 'iOS',
18+
preorderDate: undefined,
19+
};
20+
21+
// Test that all properties exist
22+
expect(mockTransaction).toHaveProperty('appTransactionID');
23+
expect(mockTransaction).toHaveProperty('bundleID');
24+
expect(mockTransaction).toHaveProperty('appVersion');
25+
expect(mockTransaction).toHaveProperty('originalAppVersion');
26+
expect(mockTransaction).toHaveProperty('originalPurchaseDate');
27+
expect(mockTransaction).toHaveProperty('deviceVerification');
28+
expect(mockTransaction).toHaveProperty('deviceVerificationNonce');
29+
expect(mockTransaction).toHaveProperty('environment');
30+
expect(mockTransaction).toHaveProperty('signedDate');
31+
expect(mockTransaction).toHaveProperty('appID');
32+
expect(mockTransaction).toHaveProperty('appVersionID');
33+
expect(mockTransaction).toHaveProperty('originalPlatform');
34+
});
35+
36+
it('should handle optional preorderDate', () => {
37+
const transactionWithPreorder = {
38+
appTransactionID: 'test-id',
39+
bundleID: 'com.example.app',
40+
appVersion: '1.0.0',
41+
originalAppVersion: '1.0.0',
42+
originalPurchaseDate: Date.now(),
43+
deviceVerification: 'verification-data',
44+
deviceVerificationNonce: 'nonce',
45+
environment: 'Production',
46+
signedDate: Date.now(),
47+
appID: 123456,
48+
appVersionID: 789012,
49+
originalPlatform: 'iOS',
50+
preorderDate: Date.now(),
51+
};
52+
53+
expect(transactionWithPreorder.preorderDate).toBeDefined();
54+
expect(typeof transactionWithPreorder.preorderDate).toBe('number');
55+
});
56+
57+
it('should have correct property types', () => {
58+
const mockTransaction = {
59+
appTransactionID: 'test-id',
60+
bundleID: 'com.example.app',
61+
appVersion: '1.0.0',
62+
originalAppVersion: '1.0.0',
63+
originalPurchaseDate: Date.now(),
64+
deviceVerification: 'verification-data',
65+
deviceVerificationNonce: 'nonce',
66+
environment: 'Production',
67+
signedDate: Date.now(),
68+
appID: 123456,
69+
appVersionID: 789012,
70+
originalPlatform: 'iOS',
71+
};
72+
73+
expect(typeof mockTransaction.appTransactionID).toBe('string');
74+
expect(typeof mockTransaction.bundleID).toBe('string');
75+
expect(typeof mockTransaction.appVersion).toBe('string');
76+
expect(typeof mockTransaction.originalPurchaseDate).toBe('number');
77+
expect(typeof mockTransaction.appID).toBe('number');
78+
});
79+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import * as ExpoIap from 'expo-iap';
2+
3+
describe('Core Functions Tests', () => {
4+
describe('Core Module Exports', () => {
5+
it('should export initConnection function', () => {
6+
expect(ExpoIap.initConnection).toBeDefined();
7+
expect(typeof ExpoIap.initConnection).toBe('function');
8+
});
9+
10+
it('should export endConnection function', () => {
11+
expect(ExpoIap.endConnection).toBeDefined();
12+
expect(typeof ExpoIap.endConnection).toBe('function');
13+
});
14+
15+
it('should export getProducts function', () => {
16+
expect(ExpoIap.getProducts).toBeDefined();
17+
expect(typeof ExpoIap.getProducts).toBe('function');
18+
});
19+
20+
it('should export getSubscriptions function', () => {
21+
expect(ExpoIap.getSubscriptions).toBeDefined();
22+
expect(typeof ExpoIap.getSubscriptions).toBe('function');
23+
});
24+
25+
it('should export requestPurchase function', () => {
26+
expect(ExpoIap.requestPurchase).toBeDefined();
27+
expect(typeof ExpoIap.requestPurchase).toBe('function');
28+
});
29+
30+
it('should export finishTransaction function', () => {
31+
expect(ExpoIap.finishTransaction).toBeDefined();
32+
expect(typeof ExpoIap.finishTransaction).toBe('function');
33+
});
34+
35+
it('should export getPurchaseHistories function', () => {
36+
expect(ExpoIap.getPurchaseHistories).toBeDefined();
37+
expect(typeof ExpoIap.getPurchaseHistories).toBe('function');
38+
});
39+
40+
it('should export getAvailablePurchases function', () => {
41+
expect(ExpoIap.getAvailablePurchases).toBeDefined();
42+
expect(typeof ExpoIap.getAvailablePurchases).toBe('function');
43+
});
44+
45+
it('should export getStorefrontIOS function', () => {
46+
expect(ExpoIap.getStorefrontIOS).toBeDefined();
47+
expect(typeof ExpoIap.getStorefrontIOS).toBe('function');
48+
});
49+
50+
it('should export deprecated getStorefront function', () => {
51+
expect(ExpoIap.getStorefront).toBeDefined();
52+
expect(typeof ExpoIap.getStorefront).toBe('function');
53+
});
54+
});
55+
56+
describe('Event Listeners', () => {
57+
it('should export purchaseUpdatedListener', () => {
58+
expect(ExpoIap.purchaseUpdatedListener).toBeDefined();
59+
expect(typeof ExpoIap.purchaseUpdatedListener).toBe('function');
60+
});
61+
62+
it('should export purchaseErrorListener', () => {
63+
expect(ExpoIap.purchaseErrorListener).toBeDefined();
64+
expect(typeof ExpoIap.purchaseErrorListener).toBe('function');
65+
});
66+
});
67+
68+
describe('Hook', () => {
69+
it('should export useIAP hook', () => {
70+
expect(ExpoIap.useIAP).toBeDefined();
71+
expect(typeof ExpoIap.useIAP).toBe('function');
72+
});
73+
});
74+
75+
describe('Android Functions', () => {
76+
it('should export deepLinkToSubscriptionsAndroid', () => {
77+
expect(ExpoIap.deepLinkToSubscriptionsAndroid).toBeDefined();
78+
expect(typeof ExpoIap.deepLinkToSubscriptionsAndroid).toBe('function');
79+
});
80+
81+
it('should export validateReceiptAndroid', () => {
82+
expect(ExpoIap.validateReceiptAndroid).toBeDefined();
83+
expect(typeof ExpoIap.validateReceiptAndroid).toBe('function');
84+
});
85+
86+
it('should export acknowledgeProductAndroid', () => {
87+
expect(ExpoIap.acknowledgeProductAndroid).toBeDefined();
88+
expect(typeof ExpoIap.acknowledgeProductAndroid).toBe('function');
89+
});
90+
91+
it('should export consumeProductAndroid', () => {
92+
expect(ExpoIap.consumeProductAndroid).toBeDefined();
93+
expect(typeof ExpoIap.consumeProductAndroid).toBe('function');
94+
});
95+
});
96+
97+
describe('Enums and Constants', () => {
98+
it('should export IapEvent enum', () => {
99+
expect(ExpoIap.IapEvent).toBeDefined();
100+
expect(ExpoIap.IapEvent.PurchaseUpdated).toBe('purchase-updated');
101+
expect(ExpoIap.IapEvent.PurchaseError).toBe('purchase-error');
102+
});
103+
104+
it('should export ErrorCode enum', () => {
105+
expect(ExpoIap.ErrorCode).toBeDefined();
106+
expect(typeof ExpoIap.ErrorCode).toBe('object');
107+
});
108+
});
109+
110+
describe('Type Guards', () => {
111+
it('should export isProductIos function', () => {
112+
expect(ExpoIap.isProductIos).toBeDefined();
113+
expect(typeof ExpoIap.isProductIos).toBe('function');
114+
});
115+
116+
it('should export isProductAndroid function', () => {
117+
expect(ExpoIap.isProductAndroid).toBeDefined();
118+
expect(typeof ExpoIap.isProductAndroid).toBe('function');
119+
});
120+
});
121+
});

example/__tests__/index.test.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import { render, waitFor } from '@testing-library/react-native';
3+
import { Platform } from 'react-native';
4+
import Home from '../app/index';
5+
6+
// Mock expo-router
7+
jest.mock('expo-router', () => ({
8+
Link: ({ children }: any) => children,
9+
}));
10+
11+
// Mock expo-iap
12+
jest.mock('expo-iap', () => ({
13+
getStorefrontIOS: jest.fn(() => Promise.resolve('US')),
14+
}));
15+
16+
describe('Home Component', () => {
17+
const originalPlatform = Platform.OS;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
afterEach(() => {
24+
Object.defineProperty(Platform, 'OS', {
25+
get: jest.fn(() => originalPlatform),
26+
configurable: true,
27+
});
28+
});
29+
30+
it('should render without crashing', () => {
31+
const { getByText } = render(<Home />);
32+
expect(getByText('expo-iap Examples')).toBeDefined();
33+
});
34+
35+
it('should render on iOS platform', async () => {
36+
// Mock Platform.OS to be iOS
37+
Object.defineProperty(Platform, 'OS', {
38+
get: jest.fn(() => 'ios'),
39+
configurable: true,
40+
});
41+
42+
const getStorefrontIOS = require('expo-iap').getStorefrontIOS;
43+
44+
const { getByText } = render(<Home />);
45+
expect(getByText('expo-iap Examples')).toBeDefined();
46+
47+
// Wait for async operations to complete
48+
await waitFor(() => {
49+
expect(getStorefrontIOS).toHaveBeenCalled();
50+
});
51+
});
52+
53+
it('should render on Android platform', () => {
54+
// Mock Platform.OS to be Android
55+
Object.defineProperty(Platform, 'OS', {
56+
get: jest.fn(() => 'android'),
57+
configurable: true,
58+
});
59+
60+
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
61+
62+
const { getByText } = render(<Home />);
63+
expect(getByText('expo-iap Examples')).toBeDefined();
64+
65+
// No async operations on Android
66+
const getStorefrontIOS = require('expo-iap').getStorefrontIOS;
67+
expect(getStorefrontIOS).not.toHaveBeenCalled();
68+
69+
consoleWarn.mockRestore();
70+
});
71+
});

0 commit comments

Comments
 (0)