expo-iap 2.8.6 → 2.9.0-rc.1

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CLAUDE.md +7 -0
  3. package/CONTRIBUTING.md +3 -4
  4. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +120 -7
  5. package/android/src/main/java/expo/modules/iap/Types.kt +1 -1
  6. package/build/helpers/subscription.d.ts.map +1 -1
  7. package/build/helpers/subscription.js +3 -6
  8. package/build/helpers/subscription.js.map +1 -1
  9. package/build/index.d.ts +31 -5
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +53 -25
  12. package/build/index.js.map +1 -1
  13. package/build/modules/android.d.ts.map +1 -1
  14. package/build/modules/android.js.map +1 -1
  15. package/build/modules/ios.d.ts.map +1 -1
  16. package/build/modules/ios.js.map +1 -1
  17. package/build/types/ExpoIapAndroid.types.d.ts +2 -2
  18. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  19. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  20. package/build/types/ExpoIapIOS.types.d.ts +3 -3
  21. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  22. package/build/types/ExpoIapIOS.types.js.map +1 -1
  23. package/build/useIAP.d.ts +12 -4
  24. package/build/useIAP.d.ts.map +1 -1
  25. package/build/useIAP.js +10 -5
  26. package/build/useIAP.js.map +1 -1
  27. package/ios/ExpoIap.podspec +1 -0
  28. package/ios/ExpoIapModule.swift +354 -1159
  29. package/jest.config.js +14 -17
  30. package/package.json +5 -3
  31. package/plugin/build/withIAP.d.ts +7 -1
  32. package/plugin/build/withIAP.js +16 -2
  33. package/plugin/build/withLocalOpenIAP.d.ts +9 -0
  34. package/plugin/build/withLocalOpenIAP.js +85 -0
  35. package/plugin/src/withIAP.ts +21 -2
  36. package/plugin/src/withLocalOpenIAP.ts +66 -0
  37. package/plugin/tsconfig.tsbuildinfo +1 -1
  38. package/src/helpers/subscription.ts +21 -28
  39. package/src/index.ts +70 -33
  40. package/src/modules/android.ts +7 -7
  41. package/src/modules/ios.ts +11 -5
  42. package/src/types/ExpoIapAndroid.types.ts +3 -4
  43. package/src/types/ExpoIapIOS.types.ts +4 -3
  44. package/src/useIAP.ts +40 -12
package/jest.config.js CHANGED
@@ -4,21 +4,18 @@ module.exports = {
4
4
  roots: ['<rootDir>/src'],
5
5
  testMatch: [
6
6
  '**/__tests__/**/*.+(ts|tsx|js)',
7
- '**/?(*.)+(spec|test).+(ts|tsx|js)',
7
+ '**/?(*.)+(spec|test).+(ts|tsx|js)'
8
8
  ],
9
9
  transform: {
10
- '^.+\\.(ts|tsx)$': [
11
- 'ts-jest',
12
- {
13
- tsconfig: {
14
- jsx: 'react',
15
- esModuleInterop: true,
16
- allowSyntheticDefaultImports: true,
17
- moduleResolution: 'node',
18
- skipLibCheck: true,
19
- },
20
- },
21
- ],
10
+ '^.+\\.(ts|tsx)$': ['ts-jest', {
11
+ tsconfig: {
12
+ jsx: 'react',
13
+ esModuleInterop: true,
14
+ allowSyntheticDefaultImports: true,
15
+ moduleResolution: 'node',
16
+ skipLibCheck: true,
17
+ }
18
+ }]
22
19
  },
23
20
  moduleNameMapper: {
24
21
  '^react-native$': '<rootDir>/src/__mocks__/react-native.js',
@@ -37,7 +34,7 @@ module.exports = {
37
34
  branches: 15,
38
35
  functions: 15,
39
36
  lines: 15,
40
- statements: 15,
41
- },
42
- },
43
- };
37
+ statements: 15
38
+ }
39
+ }
40
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "2.8.6",
3
+ "version": "2.9.0-rc.1",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -25,7 +25,8 @@
25
25
  "docs:start": "cd docs && bun run start",
26
26
  "docs:build": "cd docs && bun run build",
27
27
  "docs:serve": "cd docs && bun run serve",
28
- "docs:install": "cd docs && bun install"
28
+ "docs:install": "cd docs && bun install",
29
+ "generate:icon": "npx sharp-cli resize 32 32 -i docs/static/img/icon.png -o docs/static/img/favicon-32x32.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon-16x16.png && npx sharp-cli resize 180 180 -i docs/static/img/icon.png -o docs/static/img/apple-touch-icon.png && npx sharp-cli resize 192 192 -i docs/static/img/icon.png -o docs/static/img/android-chrome-192x192.png && npx sharp-cli resize 512 512 -i docs/static/img/icon.png -o docs/static/img/android-chrome-512x512.png && npx sharp-cli resize 150 150 -i docs/static/img/icon.png -o docs/static/img/mstile-150x150.png && npx sharp-cli resize 1200 630 -i docs/static/img/icon.png -o docs/static/img/og-image.png && npx sharp-cli resize 1200 600 -i docs/static/img/icon.png -o docs/static/img/twitter-card.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon.png && cp docs/static/img/favicon-16x16.png docs/static/img/favicon.ico"
29
30
  },
30
31
  "keywords": [
31
32
  "react-native",
@@ -60,5 +61,6 @@
60
61
  },
61
62
  "expo": {
62
63
  "plugin": "./app.plugin.js"
63
- }
64
+ },
65
+ "packageManager": "yarn@3.6.1+sha512.de524adec81a6c3d7a26d936d439d2832e351cdfc5728f9d91f3fc85dd20b04391c038e9b4ecab11cae2b0dd9f0d55fd355af766bc5c1a7f8d25d96bb2a0b2ca"
64
66
  }
@@ -1,3 +1,9 @@
1
1
  import { ConfigPlugin } from 'expo/config-plugins';
2
- declare const _default: ConfigPlugin<void>;
2
+ export interface ExpoIapPluginOptions {
3
+ /** Local development path for OpenIAP library */
4
+ localPath?: string;
5
+ /** Enable local development mode */
6
+ enableLocalDev?: boolean;
7
+ }
8
+ declare const _default: ConfigPlugin<void | ExpoIapPluginOptions>;
3
9
  export default _default;
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const config_plugins_1 = require("expo/config-plugins");
7
+ const withLocalOpenIAP_1 = __importDefault(require("./withLocalOpenIAP"));
4
8
  const pkg = require('../../package.json');
5
9
  // Global flag to prevent duplicate logs
6
10
  let hasLoggedPluginExecution = false;
@@ -65,9 +69,19 @@ const withIapAndroid = (config) => {
65
69
  });
66
70
  return config;
67
71
  };
68
- const withIap = (config, _props) => {
72
+ const withIap = (config, options) => {
69
73
  try {
70
- const result = withIapAndroid(config);
74
+ // Apply Android modifications
75
+ let result = withIapAndroid(config);
76
+ // Apply iOS local development if enabled
77
+ if (options?.enableLocalDev || options?.localPath) {
78
+ const localPath = options.localPath || '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
79
+ console.log(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
80
+ result = (0, withLocalOpenIAP_1.default)(result, { localPath });
81
+ }
82
+ else {
83
+ console.log('📦 [expo-iap] Using OpenIAP from CocoaPods');
84
+ }
71
85
  // Set flag after first execution to prevent duplicate logs
72
86
  hasLoggedPluginExecution = true;
73
87
  return result;
@@ -0,0 +1,9 @@
1
+ import { ConfigPlugin } from '@expo/config-plugins';
2
+ /**
3
+ * Plugin to add local OpenIAP pod dependency for development
4
+ * This is only for local development with openiap-apple library
5
+ */
6
+ declare const withLocalOpenIAP: ConfigPlugin<{
7
+ localPath?: string;
8
+ } | void>;
9
+ export default withLocalOpenIAP;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const config_plugins_1 = require("@expo/config-plugins");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ /**
40
+ * Plugin to add local OpenIAP pod dependency for development
41
+ * This is only for local development with openiap-apple library
42
+ */
43
+ const withLocalOpenIAP = (config, props) => {
44
+ return (0, config_plugins_1.withDangerousMod)(config, [
45
+ 'ios',
46
+ async (config) => {
47
+ const { platformProjectRoot } = config.modRequest;
48
+ const podfilePath = path.join(platformProjectRoot, 'Podfile');
49
+ // Default local path or use provided one
50
+ const localOpenIapPath = props?.localPath ||
51
+ '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
52
+ // Check if local path exists
53
+ if (!fs.existsSync(localOpenIapPath)) {
54
+ console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
55
+ console.warn(' Skipping local pod injection. Using default pod resolution.');
56
+ return config;
57
+ }
58
+ // Read Podfile
59
+ let podfileContent = fs.readFileSync(podfilePath, 'utf8');
60
+ // Check if already has the local pod reference
61
+ if (podfileContent.includes("pod 'openiap',")) {
62
+ console.log('✅ Local OpenIAP pod already configured');
63
+ return config;
64
+ }
65
+ // Find the target block and inject the local pod
66
+ const targetRegex = /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
67
+ if (targetRegex.test(podfileContent)) {
68
+ podfileContent = podfileContent.replace(targetRegex, (match) => {
69
+ return `${match}
70
+
71
+ # Local OpenIAP pod for development (added by expo-iap plugin)
72
+ pod 'openiap', :path => '${localOpenIapPath}'`;
73
+ });
74
+ // Write back to Podfile
75
+ fs.writeFileSync(podfilePath, podfileContent);
76
+ console.log(`✅ Added local OpenIAP pod at: ${localOpenIapPath}`);
77
+ }
78
+ else {
79
+ console.warn('⚠️ Could not find target block in Podfile');
80
+ }
81
+ return config;
82
+ },
83
+ ]);
84
+ };
85
+ exports.default = withLocalOpenIAP;
@@ -5,6 +5,7 @@ import {
5
5
  withAndroidManifest,
6
6
  withAppBuildGradle,
7
7
  } from 'expo/config-plugins';
8
+ import withLocalOpenIAP from './withLocalOpenIAP';
8
9
 
9
10
  const pkg = require('../../package.json');
10
11
 
@@ -98,9 +99,27 @@ const withIapAndroid: ConfigPlugin = (config) => {
98
99
  return config;
99
100
  };
100
101
 
101
- const withIap: ConfigPlugin = (config, _props) => {
102
+ export interface ExpoIapPluginOptions {
103
+ /** Local development path for OpenIAP library */
104
+ localPath?: string;
105
+ /** Enable local development mode */
106
+ enableLocalDev?: boolean;
107
+ }
108
+
109
+ const withIap: ConfigPlugin<ExpoIapPluginOptions | void> = (config, options) => {
102
110
  try {
103
- const result = withIapAndroid(config);
111
+ // Apply Android modifications
112
+ let result = withIapAndroid(config);
113
+
114
+ // Apply iOS local development if enabled
115
+ if (options?.enableLocalDev || options?.localPath) {
116
+ const localPath = options.localPath || '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
117
+ console.log(`🔧 [expo-iap] Enabling local OpenIAP development at: ${localPath}`);
118
+ result = withLocalOpenIAP(result, { localPath });
119
+ } else {
120
+ console.log('📦 [expo-iap] Using OpenIAP from CocoaPods');
121
+ }
122
+
104
123
  // Set flag after first execution to prevent duplicate logs
105
124
  hasLoggedPluginExecution = true;
106
125
  return result;
@@ -0,0 +1,66 @@
1
+ import {ConfigPlugin, withDangerousMod} from '@expo/config-plugins';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Plugin to add local OpenIAP pod dependency for development
7
+ * This is only for local development with openiap-apple library
8
+ */
9
+ const withLocalOpenIAP: ConfigPlugin<{localPath?: string} | void> = (
10
+ config,
11
+ props,
12
+ ) => {
13
+ return withDangerousMod(config, [
14
+ 'ios',
15
+ async (config) => {
16
+ const {platformProjectRoot} = config.modRequest;
17
+ const podfilePath = path.join(platformProjectRoot, 'Podfile');
18
+
19
+ // Default local path or use provided one
20
+ const localOpenIapPath =
21
+ props?.localPath ||
22
+ '/Users/crossplatformkorea/Github/hyodotdev/openiap-apple';
23
+
24
+ // Check if local path exists
25
+ if (!fs.existsSync(localOpenIapPath)) {
26
+ console.warn(`⚠️ Local openiap-apple path not found: ${localOpenIapPath}`);
27
+ console.warn(
28
+ ' Skipping local pod injection. Using default pod resolution.',
29
+ );
30
+ return config;
31
+ }
32
+
33
+ // Read Podfile
34
+ let podfileContent = fs.readFileSync(podfilePath, 'utf8');
35
+
36
+ // Check if already has the local pod reference
37
+ if (podfileContent.includes("pod 'openiap',")) {
38
+ console.log('✅ Local OpenIAP pod already configured');
39
+ return config;
40
+ }
41
+
42
+ // Find the target block and inject the local pod
43
+ const targetRegex =
44
+ /target\s+['"][\w]+['"]\s+do\s*\n\s*use_expo_modules!/;
45
+
46
+ if (targetRegex.test(podfileContent)) {
47
+ podfileContent = podfileContent.replace(targetRegex, (match) => {
48
+ return `${match}
49
+
50
+ # Local OpenIAP pod for development (added by expo-iap plugin)
51
+ pod 'openiap', :path => '${localOpenIapPath}'`;
52
+ });
53
+
54
+ // Write back to Podfile
55
+ fs.writeFileSync(podfilePath, podfileContent);
56
+ console.log(`✅ Added local OpenIAP pod at: ${localOpenIapPath}`);
57
+ } else {
58
+ console.warn('⚠️ Could not find target block in Podfile');
59
+ }
60
+
61
+ return config;
62
+ },
63
+ ]);
64
+ };
65
+
66
+ export default withLocalOpenIAP;
@@ -1 +1 @@
1
- {"root":["./src/withIAP.ts"],"version":"5.9.2"}
1
+ {"root":["./src/withiap.ts","./src/withlocalopeniap.ts"],"version":"5.9.2"}
@@ -1,5 +1,5 @@
1
- import {Platform} from 'react-native';
2
- import {getAvailablePurchases} from '../index';
1
+ import { Platform } from 'react-native';
2
+ import { getAvailablePurchases } from '../index';
3
3
 
4
4
  export interface ActiveSubscription {
5
5
  productId: string;
@@ -17,13 +17,13 @@ export interface ActiveSubscription {
17
17
  * @returns Promise<ActiveSubscription[]> array of active subscriptions with details
18
18
  */
19
19
  export const getActiveSubscriptions = async (
20
- subscriptionIds?: string[],
20
+ subscriptionIds?: string[]
21
21
  ): Promise<ActiveSubscription[]> => {
22
22
  try {
23
23
  const purchases = await getAvailablePurchases();
24
24
  const currentTime = Date.now();
25
25
  const activeSubscriptions: ActiveSubscription[] = [];
26
-
26
+
27
27
  // Filter purchases to find active subscriptions
28
28
  const filteredPurchases = purchases.filter((purchase) => {
29
29
  // If specific IDs provided, filter by them
@@ -32,17 +32,17 @@ export const getActiveSubscriptions = async (
32
32
  return false;
33
33
  }
34
34
  }
35
-
35
+
36
36
  // Check if this purchase has subscription-specific fields
37
- const hasSubscriptionFields =
37
+ const hasSubscriptionFields =
38
38
  ('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
39
- 'autoRenewingAndroid' in purchase ||
39
+ ('autoRenewingAndroid' in purchase) ||
40
40
  ('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
41
-
41
+
42
42
  if (!hasSubscriptionFields) {
43
43
  return false;
44
44
  }
45
-
45
+
46
46
  // Check if it's actually active
47
47
  if (Platform.OS === 'ios') {
48
48
  if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
@@ -53,15 +53,8 @@ export const getActiveSubscriptions = async (
53
53
  if ('environmentIOS' in purchase && purchase.environmentIOS) {
54
54
  const dayInMs = 24 * 60 * 60 * 1000;
55
55
  // If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
56
- if (
57
- !('expirationDateIOS' in purchase) ||
58
- !purchase.expirationDateIOS
59
- ) {
60
- if (
61
- purchase.environmentIOS === 'Sandbox' &&
62
- purchase.transactionDate &&
63
- currentTime - purchase.transactionDate < dayInMs
64
- ) {
56
+ if (!('expirationDateIOS' in purchase) || !purchase.expirationDateIOS) {
57
+ if (purchase.environmentIOS === 'Sandbox' && purchase.transactionDate && (currentTime - purchase.transactionDate) < dayInMs) {
65
58
  return true;
66
59
  }
67
60
  }
@@ -70,31 +63,31 @@ export const getActiveSubscriptions = async (
70
63
  // For Android, if it's in the purchases list, it's active
71
64
  return true;
72
65
  }
73
-
66
+
74
67
  return false;
75
68
  });
76
-
69
+
77
70
  // Convert to ActiveSubscription format
78
71
  for (const purchase of filteredPurchases) {
79
72
  const subscription: ActiveSubscription = {
80
73
  productId: purchase.productId,
81
74
  isActive: true,
82
75
  };
83
-
76
+
84
77
  // Add platform-specific details
85
78
  if (Platform.OS === 'ios') {
86
79
  if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
87
80
  const expirationDate = new Date(purchase.expirationDateIOS);
88
81
  subscription.expirationDateIOS = expirationDate;
89
-
82
+
90
83
  // Calculate days until expiration (round to nearest day)
91
84
  const daysUntilExpiration = Math.round(
92
- (purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
85
+ (purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24)
93
86
  );
94
87
  subscription.daysUntilExpirationIOS = daysUntilExpiration;
95
88
  subscription.willExpireSoon = daysUntilExpiration <= 7;
96
89
  }
97
-
90
+
98
91
  if ('environmentIOS' in purchase) {
99
92
  subscription.environmentIOS = purchase.environmentIOS;
100
93
  }
@@ -105,10 +98,10 @@ export const getActiveSubscriptions = async (
105
98
  subscription.willExpireSoon = !purchase.autoRenewingAndroid;
106
99
  }
107
100
  }
108
-
101
+
109
102
  activeSubscriptions.push(subscription);
110
103
  }
111
-
104
+
112
105
  return activeSubscriptions;
113
106
  } catch (error) {
114
107
  console.error('Error getting active subscriptions:', error);
@@ -122,8 +115,8 @@ export const getActiveSubscriptions = async (
122
115
  * @returns Promise<boolean> true if user has at least one active subscription
123
116
  */
124
117
  export const hasActiveSubscriptions = async (
125
- subscriptionIds?: string[],
118
+ subscriptionIds?: string[]
126
119
  ): Promise<boolean> => {
127
120
  const subscriptions = await getActiveSubscriptions(subscriptionIds);
128
121
  return subscriptions.length > 0;
129
- };
122
+ };