@parrotnavy/rn-native-updates 0.1.0

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 (46) hide show
  1. package/README.ko.md +246 -0
  2. package/README.md +245 -0
  3. package/android/build.gradle +56 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/java/com/parrotnavy/nativeupdates/NativeUpdatesExceptions.kt +33 -0
  6. package/android/src/main/java/com/parrotnavy/nativeupdates/NativeUpdatesModule.kt +171 -0
  7. package/expo-module.config.json +9 -0
  8. package/ios/NativeUpdatesExceptions.swift +26 -0
  9. package/ios/NativeUpdatesModule.swift +77 -0
  10. package/lib/module/NativeUpdatesModule.js +5 -0
  11. package/lib/module/NativeUpdatesModule.js.map +1 -0
  12. package/lib/module/api.js +163 -0
  13. package/lib/module/api.js.map +1 -0
  14. package/lib/module/hooks/index.js +4 -0
  15. package/lib/module/hooks/index.js.map +1 -0
  16. package/lib/module/hooks/useAppUpdate.js +135 -0
  17. package/lib/module/hooks/useAppUpdate.js.map +1 -0
  18. package/lib/module/index.js +6 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/types.js +157 -0
  21. package/lib/module/types.js.map +1 -0
  22. package/lib/module/versionUtils.js +18 -0
  23. package/lib/module/versionUtils.js.map +1 -0
  24. package/lib/typescript/src/NativeUpdatesModule.d.ts +3 -0
  25. package/lib/typescript/src/NativeUpdatesModule.d.ts.map +1 -0
  26. package/lib/typescript/src/api.d.ts +15 -0
  27. package/lib/typescript/src/api.d.ts.map +1 -0
  28. package/lib/typescript/src/hooks/index.d.ts +2 -0
  29. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  30. package/lib/typescript/src/hooks/useAppUpdate.d.ts +3 -0
  31. package/lib/typescript/src/hooks/useAppUpdate.d.ts.map +1 -0
  32. package/lib/typescript/src/index.d.ts +4 -0
  33. package/lib/typescript/src/index.d.ts.map +1 -0
  34. package/lib/typescript/src/types.d.ts +246 -0
  35. package/lib/typescript/src/types.d.ts.map +1 -0
  36. package/lib/typescript/src/versionUtils.d.ts +3 -0
  37. package/lib/typescript/src/versionUtils.d.ts.map +1 -0
  38. package/package.json +103 -0
  39. package/rn-native-updates.podspec +19 -0
  40. package/src/NativeUpdatesModule.ts +5 -0
  41. package/src/api.ts +239 -0
  42. package/src/hooks/index.ts +1 -0
  43. package/src/hooks/useAppUpdate.ts +189 -0
  44. package/src/index.ts +36 -0
  45. package/src/types.ts +315 -0
  46. package/src/versionUtils.ts +28 -0
package/src/api.ts ADDED
@@ -0,0 +1,239 @@
1
+ import { Linking, Platform } from 'react-native';
2
+
3
+ import { NativeUpdatesModule } from './NativeUpdatesModule';
4
+ import {
5
+ type AppStoreInfo,
6
+ AppUpdateError,
7
+ AppUpdateErrorCode,
8
+ type GetLatestVersionOptions,
9
+ type GetStoreUrlOptions,
10
+ type NeedUpdateOptions,
11
+ type NeedUpdateResult,
12
+ type PlayStoreUpdateInfo,
13
+ UpdateAvailability,
14
+ type UpdateListener,
15
+ type UpdateSubscription,
16
+ type UpdateType,
17
+ } from './types';
18
+ import { isNewerVersion } from './versionUtils';
19
+
20
+ export function getPackageName(): string {
21
+ return NativeUpdatesModule.packageName;
22
+ }
23
+
24
+ export function getCurrentVersion(): string {
25
+ return NativeUpdatesModule.currentVersion;
26
+ }
27
+
28
+ export function getCurrentBuildNumber(): number {
29
+ return Number.parseInt(NativeUpdatesModule.buildNumber, 10) || 0;
30
+ }
31
+
32
+ export function getCountry(): string {
33
+ return NativeUpdatesModule.country;
34
+ }
35
+
36
+ export async function getLatestVersion(options?: GetLatestVersionOptions): Promise<string> {
37
+ if (Platform.OS === 'ios') {
38
+ const info = await NativeUpdatesModule.getAppStoreVersion(
39
+ options?.country ?? null,
40
+ options?.forceRefresh ?? false,
41
+ );
42
+ return info.version;
43
+ }
44
+
45
+ if (Platform.OS === 'android') {
46
+ const info = await checkPlayStoreUpdate();
47
+ if (
48
+ info.updateAvailability === UpdateAvailability.UPDATE_AVAILABLE &&
49
+ info.availableVersionCode
50
+ ) {
51
+ return info.availableVersionCode.toString();
52
+ }
53
+ return getCurrentVersion();
54
+ }
55
+
56
+ throw new AppUpdateError(AppUpdateErrorCode.UNKNOWN, `Platform ${Platform.OS} is not supported`);
57
+ }
58
+
59
+ export async function getStoreUrl(options?: GetStoreUrlOptions): Promise<string> {
60
+ const packageName = getPackageName();
61
+
62
+ if (Platform.OS === 'ios') {
63
+ try {
64
+ const info = await NativeUpdatesModule.getAppStoreVersion(options?.country ?? null, false);
65
+ return info.trackViewUrl;
66
+ } catch {
67
+ const country = options?.country ?? getCountry();
68
+ return `https://apps.apple.com/${country.toLowerCase()}/app/id`;
69
+ }
70
+ }
71
+
72
+ if (Platform.OS === 'android') {
73
+ return `https://play.google.com/store/apps/details?id=${packageName}`;
74
+ }
75
+
76
+ throw new AppUpdateError(AppUpdateErrorCode.UNKNOWN, `Platform ${Platform.OS} is not supported`);
77
+ }
78
+
79
+ export async function needUpdate(options?: NeedUpdateOptions): Promise<NeedUpdateResult> {
80
+ const currentVersion = options?.currentVersion ?? getCurrentVersion();
81
+ const depth = options?.depth ?? Number.POSITIVE_INFINITY;
82
+
83
+ let latestVersion: string;
84
+ let storeUrl: string;
85
+
86
+ if (options?.latestVersion) {
87
+ latestVersion = options.latestVersion;
88
+ storeUrl = await getStoreUrl({ country: options?.country });
89
+ } else {
90
+ if (Platform.OS === 'ios') {
91
+ const info = await NativeUpdatesModule.getAppStoreVersion(
92
+ options?.country ?? null,
93
+ options?.forceRefresh ?? false,
94
+ );
95
+ latestVersion = info.version;
96
+ storeUrl = info.trackViewUrl;
97
+ } else if (Platform.OS === 'android') {
98
+ const info = await checkPlayStoreUpdate();
99
+ latestVersion = info.availableVersionCode?.toString() ?? currentVersion;
100
+ storeUrl = `https://play.google.com/store/apps/details?id=${getPackageName()}`;
101
+ } else {
102
+ throw new AppUpdateError(
103
+ AppUpdateErrorCode.UNKNOWN,
104
+ `Platform ${Platform.OS} is not supported`,
105
+ );
106
+ }
107
+ }
108
+
109
+ const isNeeded = isNewerVersion(currentVersion, latestVersion, depth);
110
+
111
+ return {
112
+ isNeeded,
113
+ currentVersion,
114
+ latestVersion,
115
+ storeUrl,
116
+ };
117
+ }
118
+
119
+ export async function openStore(options?: GetStoreUrlOptions): Promise<void> {
120
+ const url = await getStoreUrl(options);
121
+ const canOpen = await Linking.canOpenURL(url);
122
+
123
+ if (canOpen) {
124
+ await Linking.openURL(url);
125
+ } else {
126
+ throw new AppUpdateError(AppUpdateErrorCode.UNKNOWN, `Cannot open store URL: ${url}`);
127
+ }
128
+ }
129
+
130
+ export async function getAppStoreInfo(options?: GetLatestVersionOptions): Promise<AppStoreInfo> {
131
+ if (Platform.OS !== 'ios') {
132
+ throw new AppUpdateError(
133
+ AppUpdateErrorCode.UNKNOWN,
134
+ 'getAppStoreInfo is only available on iOS',
135
+ );
136
+ }
137
+
138
+ return NativeUpdatesModule.getAppStoreVersion(
139
+ options?.country ?? null,
140
+ options?.forceRefresh ?? false,
141
+ );
142
+ }
143
+
144
+ export async function checkPlayStoreUpdate(): Promise<PlayStoreUpdateInfo> {
145
+ if (Platform.OS !== 'android') {
146
+ throw new AppUpdateError(
147
+ AppUpdateErrorCode.UNKNOWN,
148
+ 'checkPlayStoreUpdate is only available on Android',
149
+ );
150
+ }
151
+
152
+ return NativeUpdatesModule.checkPlayStoreUpdate();
153
+ }
154
+
155
+ export async function startInAppUpdate(type: UpdateType): Promise<void> {
156
+ if (Platform.OS !== 'android') {
157
+ throw new AppUpdateError(
158
+ AppUpdateErrorCode.UNKNOWN,
159
+ 'startInAppUpdate is only available on Android',
160
+ );
161
+ }
162
+
163
+ return NativeUpdatesModule.startUpdate(type);
164
+ }
165
+
166
+ export function completeInAppUpdate(): void {
167
+ if (Platform.OS !== 'android') {
168
+ throw new AppUpdateError(
169
+ AppUpdateErrorCode.UNKNOWN,
170
+ 'completeInAppUpdate is only available on Android',
171
+ );
172
+ }
173
+
174
+ NativeUpdatesModule.completeUpdate();
175
+ }
176
+
177
+ let eventSubscription: { remove: () => void } | null = null;
178
+ const listeners = new Set<UpdateListener>();
179
+
180
+ function setupEventListener(): void {
181
+ if (eventSubscription || Platform.OS !== 'android') return;
182
+
183
+ const { EventEmitter } = require('expo-modules-core');
184
+ const emitter = new EventEmitter(NativeUpdatesModule);
185
+
186
+ const handleEvent = (_eventName: string, data: Record<string, unknown>) => {
187
+ const state = {
188
+ installStatus: (data.installStatus as number) ?? 0,
189
+ bytesDownloaded: (data.bytesDownloaded as number) ?? 0,
190
+ totalBytesToDownload: (data.totalBytesToDownload as number) ?? 0,
191
+ downloadProgress: (data.downloadProgress as number) ?? 0,
192
+ };
193
+
194
+ for (const listener of listeners) {
195
+ listener(state);
196
+ }
197
+ };
198
+
199
+ const progressSub = emitter.addListener('onUpdateProgress', (data: Record<string, unknown>) =>
200
+ handleEvent('onUpdateProgress', data),
201
+ );
202
+ const downloadedSub = emitter.addListener('onUpdateDownloaded', (data: Record<string, unknown>) =>
203
+ handleEvent('onUpdateDownloaded', data),
204
+ );
205
+ const installedSub = emitter.addListener('onUpdateInstalled', (data: Record<string, unknown>) =>
206
+ handleEvent('onUpdateInstalled', data),
207
+ );
208
+ const failedSub = emitter.addListener('onUpdateFailed', (data: Record<string, unknown>) =>
209
+ handleEvent('onUpdateFailed', data),
210
+ );
211
+
212
+ eventSubscription = {
213
+ remove: () => {
214
+ progressSub.remove();
215
+ downloadedSub.remove();
216
+ installedSub.remove();
217
+ failedSub.remove();
218
+ eventSubscription = null;
219
+ },
220
+ };
221
+ }
222
+
223
+ export function addUpdateListener(listener: UpdateListener): UpdateSubscription {
224
+ if (Platform.OS !== 'android') {
225
+ return { remove: () => {} };
226
+ }
227
+
228
+ setupEventListener();
229
+ listeners.add(listener);
230
+
231
+ return {
232
+ remove: () => {
233
+ listeners.delete(listener);
234
+ if (listeners.size === 0 && eventSubscription) {
235
+ eventSubscription.remove();
236
+ }
237
+ },
238
+ };
239
+ }
@@ -0,0 +1 @@
1
+ export { useAppUpdate } from './useAppUpdate';
@@ -0,0 +1,189 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Platform } from 'react-native';
3
+
4
+ import {
5
+ addUpdateListener,
6
+ checkPlayStoreUpdate,
7
+ completeInAppUpdate,
8
+ getCurrentVersion,
9
+ needUpdate,
10
+ openStore,
11
+ startInAppUpdate,
12
+ } from '../api';
13
+ import {
14
+ AppUpdateError,
15
+ AppUpdateErrorCode,
16
+ InstallStatus,
17
+ type PlayStoreUpdateInfo,
18
+ UpdateAvailability,
19
+ type UpdateSubscription,
20
+ type UpdateType,
21
+ type UseAppUpdateOptions,
22
+ type UseAppUpdateResult,
23
+ } from '../types';
24
+
25
+ export function useAppUpdate(options?: UseAppUpdateOptions): UseAppUpdateResult {
26
+ const [isChecking, setIsChecking] = useState(false);
27
+ const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
28
+ const [currentVersion] = useState(getCurrentVersion);
29
+ const [latestVersion, setLatestVersion] = useState<string | null>(null);
30
+ const [storeUrl, setStoreUrl] = useState<string | null>(null);
31
+ const [error, setError] = useState<AppUpdateError | null>(null);
32
+
33
+ const [isDownloading, setIsDownloading] = useState(false);
34
+ const [downloadProgress, setDownloadProgress] = useState(0);
35
+ const [isReadyToInstall, setIsReadyToInstall] = useState(false);
36
+ const [playStoreInfo, setPlayStoreInfo] = useState<PlayStoreUpdateInfo | null>(null);
37
+
38
+ const subscriptionRef = useRef<UpdateSubscription | null>(null);
39
+ const mountedRef = useRef(true);
40
+
41
+ const checkUpdate = useCallback(async () => {
42
+ if (!mountedRef.current) return;
43
+
44
+ setIsChecking(true);
45
+ setError(null);
46
+
47
+ try {
48
+ const result = await needUpdate({
49
+ country: options?.country,
50
+ forceRefresh: true,
51
+ });
52
+
53
+ if (!mountedRef.current) return;
54
+
55
+ setIsUpdateAvailable(result.isNeeded);
56
+ setLatestVersion(result.latestVersion);
57
+ setStoreUrl(result.storeUrl);
58
+
59
+ if (Platform.OS === 'android') {
60
+ try {
61
+ const info = await checkPlayStoreUpdate();
62
+ if (mountedRef.current) {
63
+ setPlayStoreInfo(info);
64
+ setIsUpdateAvailable(info.updateAvailability === UpdateAvailability.UPDATE_AVAILABLE);
65
+ }
66
+ } catch {
67
+ if (mountedRef.current) {
68
+ setPlayStoreInfo(null);
69
+ }
70
+ }
71
+ }
72
+ } catch (e) {
73
+ if (!mountedRef.current) return;
74
+
75
+ const appError =
76
+ e instanceof AppUpdateError
77
+ ? e
78
+ : new AppUpdateError(AppUpdateErrorCode.CHECK_FAILED, String(e));
79
+
80
+ setError(appError);
81
+ options?.onError?.(appError);
82
+ } finally {
83
+ if (mountedRef.current) {
84
+ setIsChecking(false);
85
+ }
86
+ }
87
+ }, [options?.country, options?.onError]);
88
+
89
+ const handleOpenStore = useCallback(async () => {
90
+ try {
91
+ await openStore({ country: options?.country });
92
+ } catch (e) {
93
+ const appError =
94
+ e instanceof AppUpdateError ? e : new AppUpdateError(AppUpdateErrorCode.UNKNOWN, String(e));
95
+ setError(appError);
96
+ options?.onError?.(appError);
97
+ }
98
+ }, [options?.country, options?.onError]);
99
+
100
+ const handleStartUpdate = useCallback(
101
+ async (type: UpdateType) => {
102
+ if (Platform.OS !== 'android') return;
103
+
104
+ setError(null);
105
+
106
+ subscriptionRef.current?.remove();
107
+ subscriptionRef.current = addUpdateListener((state) => {
108
+ if (!mountedRef.current) return;
109
+
110
+ setDownloadProgress(state.downloadProgress);
111
+
112
+ if (state.installStatus === InstallStatus.DOWNLOADING) {
113
+ setIsDownloading(true);
114
+ setIsReadyToInstall(false);
115
+ } else if (state.installStatus === InstallStatus.DOWNLOADED) {
116
+ setIsDownloading(false);
117
+ setIsReadyToInstall(true);
118
+ } else if (
119
+ state.installStatus === InstallStatus.INSTALLED ||
120
+ state.installStatus === InstallStatus.FAILED ||
121
+ state.installStatus === InstallStatus.CANCELED
122
+ ) {
123
+ setIsDownloading(false);
124
+ if (state.installStatus !== InstallStatus.INSTALLED) {
125
+ setIsReadyToInstall(false);
126
+ }
127
+ }
128
+ });
129
+
130
+ try {
131
+ await startInAppUpdate(type);
132
+ } catch (e) {
133
+ const appError =
134
+ e instanceof AppUpdateError
135
+ ? e
136
+ : new AppUpdateError(AppUpdateErrorCode.UPDATE_FAILED, String(e));
137
+ setError(appError);
138
+ options?.onError?.(appError);
139
+ }
140
+ },
141
+ [options?.onError],
142
+ );
143
+
144
+ const handleCompleteUpdate = useCallback(async () => {
145
+ if (Platform.OS !== 'android') return;
146
+
147
+ try {
148
+ completeInAppUpdate();
149
+ } catch (e) {
150
+ const appError =
151
+ e instanceof AppUpdateError
152
+ ? e
153
+ : new AppUpdateError(AppUpdateErrorCode.UPDATE_FAILED, String(e));
154
+ setError(appError);
155
+ options?.onError?.(appError);
156
+ }
157
+ }, [options?.onError]);
158
+
159
+ // biome-ignore lint/correctness/useExhaustiveDependencies: checkOnMount is intentionally only checked once on mount
160
+ useEffect(() => {
161
+ mountedRef.current = true;
162
+
163
+ if (options?.checkOnMount) {
164
+ checkUpdate();
165
+ }
166
+
167
+ return () => {
168
+ mountedRef.current = false;
169
+ subscriptionRef.current?.remove();
170
+ };
171
+ }, []);
172
+
173
+ return {
174
+ isChecking,
175
+ isUpdateAvailable,
176
+ currentVersion,
177
+ latestVersion,
178
+ storeUrl,
179
+ error,
180
+ isDownloading,
181
+ downloadProgress,
182
+ isReadyToInstall,
183
+ playStoreInfo,
184
+ checkUpdate,
185
+ openStore: handleOpenStore,
186
+ startUpdate: handleStartUpdate,
187
+ completeUpdate: handleCompleteUpdate,
188
+ };
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export {
2
+ addUpdateListener,
3
+ checkPlayStoreUpdate,
4
+ completeInAppUpdate,
5
+ getAppStoreInfo,
6
+ getCountry,
7
+ getCurrentBuildNumber,
8
+ getCurrentVersion,
9
+ getLatestVersion,
10
+ getPackageName,
11
+ getStoreUrl,
12
+ needUpdate,
13
+ openStore,
14
+ startInAppUpdate,
15
+ } from './api';
16
+ export { useAppUpdate } from './hooks';
17
+ export {
18
+ type AppStoreInfo,
19
+ AppUpdateError,
20
+ AppUpdateErrorCode,
21
+ type GetLatestVersionOptions,
22
+ type GetStoreUrlOptions,
23
+ type InstallState,
24
+ InstallStatus,
25
+ type NativeConstants,
26
+ type NativeUpdatesModuleType,
27
+ type NeedUpdateOptions,
28
+ type NeedUpdateResult,
29
+ type PlayStoreUpdateInfo,
30
+ UpdateAvailability,
31
+ type UpdateListener,
32
+ type UpdateSubscription,
33
+ UpdateType,
34
+ type UseAppUpdateOptions,
35
+ type UseAppUpdateResult,
36
+ } from './types';