@hot-updater/react-native 0.30.10 → 0.30.12

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 (54) hide show
  1. package/lib/commonjs/DefaultResolver.js +13 -3
  2. package/lib/commonjs/DefaultResolver.js.map +1 -1
  3. package/lib/commonjs/DefaultResolver.spec.js +60 -0
  4. package/lib/commonjs/DefaultResolver.spec.js.map +1 -0
  5. package/lib/commonjs/error.js +83 -0
  6. package/lib/commonjs/error.js.map +1 -1
  7. package/lib/commonjs/fetchUpdateInfo.js +8 -10
  8. package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/index.spec.js +16 -0
  11. package/lib/commonjs/index.spec.js.map +1 -1
  12. package/lib/commonjs/types.js.map +1 -1
  13. package/lib/commonjs/wrap.js.map +1 -1
  14. package/lib/module/DefaultResolver.js +13 -3
  15. package/lib/module/DefaultResolver.js.map +1 -1
  16. package/lib/module/DefaultResolver.spec.js +60 -0
  17. package/lib/module/DefaultResolver.spec.js.map +1 -0
  18. package/lib/module/error.js +83 -0
  19. package/lib/module/error.js.map +1 -1
  20. package/lib/module/fetchUpdateInfo.js +8 -10
  21. package/lib/module/fetchUpdateInfo.js.map +1 -1
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/index.spec.js +16 -0
  24. package/lib/module/index.spec.js.map +1 -1
  25. package/lib/module/types.js.map +1 -1
  26. package/lib/module/wrap.js.map +1 -1
  27. package/lib/typescript/commonjs/DefaultResolver.d.ts +4 -3
  28. package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts +1 -2
  30. package/lib/typescript/commonjs/fetchUpdateInfo.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/index.d.ts +1 -1
  32. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/types.d.ts +1 -0
  34. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/wrap.d.ts +5 -3
  36. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  37. package/lib/typescript/module/DefaultResolver.d.ts +4 -3
  38. package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
  39. package/lib/typescript/module/fetchUpdateInfo.d.ts +1 -2
  40. package/lib/typescript/module/fetchUpdateInfo.d.ts.map +1 -1
  41. package/lib/typescript/module/index.d.ts +1 -1
  42. package/lib/typescript/module/index.d.ts.map +1 -1
  43. package/lib/typescript/module/types.d.ts +1 -0
  44. package/lib/typescript/module/types.d.ts.map +1 -1
  45. package/lib/typescript/module/wrap.d.ts +5 -3
  46. package/lib/typescript/module/wrap.d.ts.map +1 -1
  47. package/package.json +11 -18
  48. package/src/DefaultResolver.spec.ts +78 -0
  49. package/src/DefaultResolver.ts +24 -5
  50. package/src/fetchUpdateInfo.ts +9 -12
  51. package/src/index.spec.ts +21 -1
  52. package/src/index.ts +1 -0
  53. package/src/types.ts +2 -0
  54. package/src/wrap.tsx +5 -3
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { createDefaultResolver } from "./DefaultResolver";
4
+ import type { ResolverCheckUpdateParams } from "./types";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ fetchUpdateInfo: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("./fetchUpdateInfo", () => ({
11
+ fetchUpdateInfo: mocks.fetchUpdateInfo,
12
+ }));
13
+
14
+ const createParams = (
15
+ params?: Partial<ResolverCheckUpdateParams>,
16
+ ): ResolverCheckUpdateParams => ({
17
+ appVersion: "1.0.0",
18
+ bundleId: "bundle-id",
19
+ channel: "production",
20
+ cohort: "cohort",
21
+ fingerprintHash: null,
22
+ minBundleId: "min-bundle-id",
23
+ platform: "ios",
24
+ updateStrategy: "appVersion",
25
+ ...params,
26
+ });
27
+
28
+ describe("createDefaultResolver", () => {
29
+ beforeEach(() => {
30
+ mocks.fetchUpdateInfo.mockReset();
31
+ mocks.fetchUpdateInfo.mockResolvedValue(null);
32
+ });
33
+
34
+ it("resolves dynamic baseURL before checking for updates", async () => {
35
+ const resolveBaseURL = vi.fn(async () => "https://updates.example.com");
36
+ const resolver = createDefaultResolver(resolveBaseURL);
37
+
38
+ await resolver.checkUpdate?.(createParams());
39
+
40
+ expect(resolveBaseURL).toHaveBeenCalledWith();
41
+ expect(mocks.fetchUpdateInfo).toHaveBeenCalledWith({
42
+ requestHeaders: undefined,
43
+ requestTimeout: undefined,
44
+ url: "https://updates.example.com/app-version/ios/1.0.0/production/min-bundle-id/bundle-id/cohort",
45
+ });
46
+ });
47
+
48
+ it("calls dynamic baseURL for each update check", async () => {
49
+ const resolveBaseURL = vi
50
+ .fn<() => Promise<string>>()
51
+ .mockResolvedValueOnce("https://one.example.com")
52
+ .mockResolvedValueOnce("https://two.example.com");
53
+ const resolver = createDefaultResolver(resolveBaseURL);
54
+
55
+ await resolver.checkUpdate?.(createParams());
56
+ await resolver.checkUpdate?.(createParams());
57
+
58
+ expect(resolveBaseURL).toHaveBeenCalledTimes(2);
59
+ expect(mocks.fetchUpdateInfo).toHaveBeenNthCalledWith(1, {
60
+ requestHeaders: undefined,
61
+ requestTimeout: undefined,
62
+ url: "https://one.example.com/app-version/ios/1.0.0/production/min-bundle-id/bundle-id/cohort",
63
+ });
64
+ expect(mocks.fetchUpdateInfo).toHaveBeenNthCalledWith(2, {
65
+ requestHeaders: undefined,
66
+ requestTimeout: undefined,
67
+ url: "https://two.example.com/app-version/ios/1.0.0/production/min-bundle-id/bundle-id/cohort",
68
+ });
69
+ });
70
+
71
+ it("rejects an empty dynamic baseURL", async () => {
72
+ const resolver = createDefaultResolver(() => "");
73
+
74
+ await expect(resolver.checkUpdate?.(createParams())).rejects.toThrow(
75
+ "baseURL resolver must return a non-empty string",
76
+ );
77
+ });
78
+ });
@@ -1,20 +1,39 @@
1
1
  import type { AppUpdateInfo } from "@hot-updater/core";
2
2
 
3
3
  import { fetchUpdateInfo } from "./fetchUpdateInfo";
4
- import type { HotUpdaterResolver, ResolverCheckUpdateParams } from "./types";
4
+ import type {
5
+ HotUpdaterBaseURL,
6
+ HotUpdaterResolver,
7
+ ResolverCheckUpdateParams,
8
+ } from "./types";
9
+
10
+ const resolveBaseURL = async (baseURL: HotUpdaterBaseURL): Promise<string> => {
11
+ const resolvedBaseURL =
12
+ typeof baseURL === "function" ? await baseURL() : baseURL;
13
+
14
+ if (!resolvedBaseURL) {
15
+ throw new Error("baseURL resolver must return a non-empty string");
16
+ }
17
+
18
+ return resolvedBaseURL;
19
+ };
5
20
 
6
21
  /**
7
22
  * Creates a default resolver that uses baseURL for network operations.
8
23
  * This encapsulates the existing baseURL logic into a resolver.
9
24
  *
10
- * @param baseURL - The base URL for the update server
25
+ * @param baseURL - The base URL for the update server, or a function that
26
+ * resolves it before each update check.
11
27
  * @returns A HotUpdaterResolver that uses the baseURL
12
28
  */
13
- export function createDefaultResolver(baseURL: string): HotUpdaterResolver {
29
+ export function createDefaultResolver(
30
+ baseURL: HotUpdaterBaseURL,
31
+ ): HotUpdaterResolver {
14
32
  return {
15
33
  checkUpdate: async (
16
34
  params: ResolverCheckUpdateParams,
17
35
  ): Promise<AppUpdateInfo | null> => {
36
+ const resolvedBaseURL = await resolveBaseURL(baseURL);
18
37
  let url: string;
19
38
  const cohortPath = `/${encodeURIComponent(params.cohort)}`;
20
39
 
@@ -22,9 +41,9 @@ export function createDefaultResolver(baseURL: string): HotUpdaterResolver {
22
41
  if (!params.fingerprintHash) {
23
42
  throw new Error("Fingerprint hash is required");
24
43
  }
25
- url = `${baseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
44
+ url = `${resolvedBaseURL}/fingerprint/${params.platform}/${params.fingerprintHash}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
26
45
  } else {
27
- url = `${baseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
46
+ url = `${resolvedBaseURL}/app-version/${params.platform}/${params.appVersion}/${params.channel}/${params.minBundleId}/${params.bundleId}${cohortPath}`;
28
47
  }
29
48
 
30
49
  return fetchUpdateInfo({
@@ -3,20 +3,18 @@ import type { AppUpdateInfo } from "@hot-updater/core";
3
3
  export const fetchUpdateInfo = async ({
4
4
  url,
5
5
  requestHeaders,
6
- onError,
7
6
  requestTimeout = 5000,
8
7
  }: {
9
8
  url: string;
10
9
  requestHeaders?: Record<string, string>;
11
- onError?: (error: Error) => void;
12
10
  requestTimeout?: number;
13
11
  }): Promise<AppUpdateInfo | null> => {
14
- try {
15
- const controller = new AbortController();
16
- const timeoutId = setTimeout(() => {
17
- controller.abort();
18
- }, requestTimeout);
12
+ const controller = new AbortController();
13
+ const timeoutId = setTimeout(() => {
14
+ controller.abort();
15
+ }, requestTimeout);
19
16
 
17
+ try {
20
18
  const headers = {
21
19
  "Content-Type": "application/json",
22
20
  ...requestHeaders,
@@ -26,7 +24,6 @@ export const fetchUpdateInfo = async ({
26
24
  signal: controller.signal,
27
25
  headers,
28
26
  });
29
- clearTimeout(timeoutId);
30
27
 
31
28
  if (response.status !== 200) {
32
29
  throw new Error(response.statusText);
@@ -34,10 +31,10 @@ export const fetchUpdateInfo = async ({
34
31
  return response.json();
35
32
  } catch (error: unknown) {
36
33
  if (error instanceof Error && error.name === "AbortError") {
37
- onError?.(new Error("Request timed out"));
38
- } else {
39
- onError?.(error as Error);
34
+ throw new Error("Request timed out");
40
35
  }
41
- return null;
36
+ throw error;
37
+ } finally {
38
+ clearTimeout(timeoutId);
42
39
  }
43
40
  };
package/src/index.spec.ts CHANGED
@@ -74,7 +74,7 @@ describe("HotUpdater client initialization", () => {
74
74
 
75
75
  mocks.addListener.mockReturnValue(() => {});
76
76
  mocks.checkForUpdate.mockResolvedValue(null);
77
- mocks.createDefaultResolver.mockImplementation((baseURL: string) => ({
77
+ mocks.createDefaultResolver.mockImplementation((baseURL: unknown) => ({
78
78
  baseURL,
79
79
  checkUpdate: vi.fn(),
80
80
  notifyAppReady: vi.fn(),
@@ -114,6 +114,26 @@ describe("HotUpdater client initialization", () => {
114
114
  });
115
115
  });
116
116
 
117
+ it("accepts dynamic baseURL resolvers for manual update flows", async () => {
118
+ const resolver = {
119
+ checkUpdate: vi.fn(),
120
+ notifyAppReady: vi.fn(),
121
+ };
122
+ const resolveBaseURL = vi.fn(() => "https://updates.example.com");
123
+ mocks.createDefaultResolver.mockReturnValue(resolver);
124
+
125
+ const HotUpdater = await importHotUpdater();
126
+
127
+ HotUpdater.init({
128
+ baseURL: resolveBaseURL,
129
+ });
130
+
131
+ expect(mocks.createDefaultResolver).toHaveBeenCalledWith(resolveBaseURL);
132
+ expect(mocks.init).toHaveBeenCalledWith({
133
+ resolver,
134
+ });
135
+ });
136
+
117
137
  it("uses init configuration for later manual update checks", async () => {
118
138
  const resolver = {
119
139
  checkUpdate: vi.fn(),
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export type {
48
48
  export * from "./store";
49
49
  export {
50
50
  extractSignatureFailure,
51
+ type HotUpdaterBaseURL,
51
52
  type HotUpdaterResolver,
52
53
  isSignatureVerificationError,
53
54
  type ResolverCheckUpdateParams,
package/src/types.ts CHANGED
@@ -2,6 +2,8 @@ import type { AppUpdateInfo } from "@hot-updater/core";
2
2
 
3
3
  import type { NotifyAppReadyResult } from "./native";
4
4
 
5
+ export type HotUpdaterBaseURL = string | (() => string | Promise<string>);
6
+
5
7
  /**
6
8
  * Parameters passed to resolver.checkUpdate method
7
9
  */
package/src/wrap.tsx CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  reload,
11
11
  } from "./native";
12
12
  import { useHotUpdaterStore } from "./store";
13
- import type { HotUpdaterResolver } from "./types";
13
+ import type { HotUpdaterBaseURL, HotUpdaterResolver } from "./types";
14
14
 
15
15
  export interface RunUpdateProcessResponse {
16
16
  status: "ROLLBACK" | "UPDATE" | "UP_TO_DATE";
@@ -70,10 +70,12 @@ interface CommonHotUpdaterOptions {
70
70
  */
71
71
  interface BaseURLConfig {
72
72
  /**
73
- * Base URL for update server
73
+ * Base URL for update server. Use a function to resolve it dynamically
74
+ * before each update check.
74
75
  * @example "https://update.example.com"
76
+ * @example () => getUpdateServerURL()
75
77
  */
76
- baseURL: string;
78
+ baseURL: HotUpdaterBaseURL;
77
79
 
78
80
  /**
79
81
  * Resolver is not allowed when using baseURL