@youversion/platform-react-hooks 0.6.0 → 0.8.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 (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +56 -0
  3. package/README.md +3 -3
  4. package/dist/__tests__/mocks/auth.d.ts +12 -0
  5. package/dist/__tests__/mocks/auth.d.ts.map +1 -0
  6. package/dist/__tests__/mocks/auth.js +44 -0
  7. package/dist/__tests__/mocks/auth.js.map +1 -0
  8. package/dist/__tests__/utils/test-utils.d.ts +15 -0
  9. package/dist/__tests__/utils/test-utils.d.ts.map +1 -0
  10. package/dist/__tests__/utils/test-utils.js +24 -0
  11. package/dist/__tests__/utils/test-utils.js.map +1 -0
  12. package/dist/context/YouVersionAuthContext.d.ts +4 -0
  13. package/dist/context/YouVersionAuthContext.d.ts.map +1 -0
  14. package/dist/context/YouVersionAuthContext.js +13 -0
  15. package/dist/context/YouVersionAuthContext.js.map +1 -0
  16. package/dist/context/YouVersionAuthProvider.d.ts +8 -0
  17. package/dist/context/YouVersionAuthProvider.d.ts.map +1 -0
  18. package/dist/context/YouVersionAuthProvider.js +80 -0
  19. package/dist/context/YouVersionAuthProvider.js.map +1 -0
  20. package/dist/context/YouVersionContext.d.ts +8 -0
  21. package/dist/context/YouVersionContext.d.ts.map +1 -0
  22. package/dist/context/YouVersionContext.js +4 -0
  23. package/dist/context/YouVersionContext.js.map +1 -0
  24. package/dist/context/YouVersionProvider.d.ts +17 -0
  25. package/dist/context/YouVersionProvider.d.ts.map +1 -0
  26. package/dist/context/YouVersionProvider.js +25 -0
  27. package/dist/context/YouVersionProvider.js.map +1 -0
  28. package/dist/context/index.d.ts +2 -2
  29. package/dist/context/index.d.ts.map +1 -1
  30. package/dist/context/index.js +2 -2
  31. package/dist/context/index.js.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/types/auth.d.ts +18 -0
  37. package/dist/types/auth.d.ts.map +1 -0
  38. package/dist/types/auth.js +2 -0
  39. package/dist/types/auth.js.map +1 -0
  40. package/dist/useBibleClient.js +3 -3
  41. package/dist/useBibleClient.js.map +1 -1
  42. package/dist/useHighlights.js +3 -3
  43. package/dist/useHighlights.js.map +1 -1
  44. package/dist/useLanguages.js +3 -3
  45. package/dist/useLanguages.js.map +1 -1
  46. package/dist/usePassage.d.ts +3 -3
  47. package/dist/usePassage.d.ts.map +1 -1
  48. package/dist/usePassage.js +5 -5
  49. package/dist/usePassage.js.map +1 -1
  50. package/dist/useYVAuth.d.ts +97 -0
  51. package/dist/useYVAuth.d.ts.map +1 -0
  52. package/dist/useYVAuth.js +135 -0
  53. package/dist/useYVAuth.js.map +1 -0
  54. package/package.json +3 -3
  55. package/src/__tests__/mocks/auth.ts +48 -0
  56. package/src/__tests__/utils/test-utils.tsx +43 -0
  57. package/src/context/YouVersionAuthContext.test.tsx +88 -0
  58. package/src/context/YouVersionAuthContext.tsx +20 -0
  59. package/src/context/YouVersionAuthProvider.test.tsx +373 -0
  60. package/src/context/YouVersionAuthProvider.tsx +90 -0
  61. package/src/context/{BibleSDKContext.tsx → YouVersionContext.tsx} +2 -2
  62. package/src/context/YouVersionProvider.tsx +65 -0
  63. package/src/context/index.ts +2 -2
  64. package/src/index.ts +4 -0
  65. package/src/types/auth.ts +20 -0
  66. package/src/useBibleClient.test.tsx +14 -14
  67. package/src/useBibleClient.ts +3 -3
  68. package/src/useHighlights.test.tsx +6 -6
  69. package/src/useHighlights.ts +3 -3
  70. package/src/useLanguages.test.tsx +6 -6
  71. package/src/useLanguages.ts +3 -3
  72. package/src/usePassage.ts +8 -15
  73. package/src/useVOTD.test.tsx +6 -6
  74. package/src/useYVAuth.test.tsx +378 -0
  75. package/src/useYVAuth.ts +179 -0
  76. package/dist/context/BibleSDKContext.d.ts +0 -8
  77. package/dist/context/BibleSDKContext.d.ts.map +0 -1
  78. package/dist/context/BibleSDKContext.js +0 -4
  79. package/dist/context/BibleSDKContext.js.map +0 -1
  80. package/dist/context/BibleSDKProvider.d.ts +0 -9
  81. package/dist/context/BibleSDKProvider.d.ts.map +0 -1
  82. package/dist/context/BibleSDKProvider.js +0 -18
  83. package/dist/context/BibleSDKProvider.js.map +0 -1
  84. package/src/context/BibleSDKProvider.tsx +0 -35
@@ -1,16 +1,16 @@
1
1
  'use client';
2
2
 
3
3
  import { useContext, useMemo } from 'react';
4
- import { BibleSDKContext } from './context';
4
+ import { YouVersionContext } from './context';
5
5
  import { BibleClient, ApiClient } from '@youversion/platform-core';
6
6
 
7
7
  export function useBibleClient(): BibleClient {
8
- const context = useContext(BibleSDKContext);
8
+ const context = useContext(YouVersionContext);
9
9
 
10
10
  return useMemo(() => {
11
11
  if (!context?.appKey) {
12
12
  throw new Error(
13
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
13
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
14
14
  );
15
15
  }
16
16
 
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useHighlights } from './useHighlights';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import {
7
7
  HighlightsClient,
8
8
  ApiClient,
@@ -56,7 +56,7 @@ describe('useHighlights', () => {
56
56
 
57
57
  const createWrapper = (contextValue: { appKey: string }) => {
58
58
  return ({ children }: { children: ReactNode }) => (
59
- <BibleSDKContext.Provider value={contextValue}>{children}</BibleSDKContext.Provider>
59
+ <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
60
60
  );
61
61
  };
62
62
 
@@ -85,7 +85,7 @@ describe('useHighlights', () => {
85
85
  describe('context validation', () => {
86
86
  it('should throw error when context is not provided', () => {
87
87
  expect(() => renderHook(() => useHighlights())).toThrow(
88
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
88
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
89
89
  );
90
90
  });
91
91
 
@@ -95,7 +95,7 @@ describe('useHighlights', () => {
95
95
  });
96
96
 
97
97
  expect(() => renderHook(() => useHighlights(), { wrapper })).toThrow(
98
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
98
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
99
99
  );
100
100
  });
101
101
  });
@@ -131,13 +131,13 @@ describe('useHighlights', () => {
131
131
  let currentAppKey = mockAppKey;
132
132
 
133
133
  const wrapper = ({ children }: { children: ReactNode }) => (
134
- <BibleSDKContext.Provider
134
+ <YouVersionContext.Provider
135
135
  value={{
136
136
  appKey: currentAppKey,
137
137
  }}
138
138
  >
139
139
  {children}
140
- </BibleSDKContext.Provider>
140
+ </YouVersionContext.Provider>
141
141
  );
142
142
 
143
143
  const { rerender } = renderHook(() => useHighlights(), { wrapper });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useMemo, useCallback } from 'react';
4
4
  import { useContext } from 'react';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import { HighlightsClient, ApiClient } from '@youversion/platform-core';
7
7
  import { useApiData, type UseApiDataOptions } from './useApiData';
8
8
  import {
@@ -24,12 +24,12 @@ export function useHighlights(
24
24
  createHighlight: (data: CreateHighlight) => Promise<Highlight>;
25
25
  deleteHighlight: (passageId: string, deleteOptions?: DeleteHighlightOptions) => Promise<void>;
26
26
  } {
27
- const context = useContext(BibleSDKContext);
27
+ const context = useContext(YouVersionContext);
28
28
 
29
29
  const highlightsClient = useMemo(() => {
30
30
  if (!context?.appKey) {
31
31
  throw new Error(
32
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
32
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
33
33
  );
34
34
  }
35
35
  return new HighlightsClient(
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useLanguages } from './useLanguages';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import {
7
7
  LanguagesClient,
8
8
  ApiClient,
@@ -74,7 +74,7 @@ describe('useLanguages', () => {
74
74
 
75
75
  const createWrapper = (contextValue: { appKey: string }) => {
76
76
  return ({ children }: { children: ReactNode }) => (
77
- <BibleSDKContext.Provider value={contextValue}>{children}</BibleSDKContext.Provider>
77
+ <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
78
78
  );
79
79
  };
80
80
 
@@ -99,7 +99,7 @@ describe('useLanguages', () => {
99
99
  describe('context validation', () => {
100
100
  it('should throw error when context is not provided', () => {
101
101
  expect(() => renderHook(() => useLanguages({ country: 'US' }))).toThrow(
102
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
102
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
103
103
  );
104
104
  });
105
105
 
@@ -109,7 +109,7 @@ describe('useLanguages', () => {
109
109
  });
110
110
 
111
111
  expect(() => renderHook(() => useLanguages({ country: 'US' }), { wrapper })).toThrow(
112
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
112
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
113
113
  );
114
114
  });
115
115
  });
@@ -146,13 +146,13 @@ describe('useLanguages', () => {
146
146
  let currentAppKey = mockAppKey;
147
147
 
148
148
  const wrapper = ({ children }: { children: ReactNode }) => (
149
- <BibleSDKContext.Provider
149
+ <YouVersionContext.Provider
150
150
  value={{
151
151
  appKey: currentAppKey,
152
152
  }}
153
153
  >
154
154
  {children}
155
- </BibleSDKContext.Provider>
155
+ </YouVersionContext.Provider>
156
156
  );
157
157
 
158
158
  const { rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useMemo } from 'react';
4
4
  import { useContext } from 'react';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import { LanguagesClient, ApiClient } from '@youversion/platform-core';
7
7
  import { useApiData, type UseApiDataOptions } from './useApiData';
8
8
  import {
@@ -20,12 +20,12 @@ export function useLanguages(
20
20
  error: Error | null;
21
21
  refetch: () => void;
22
22
  } {
23
- const context = useContext(BibleSDKContext);
23
+ const context = useContext(YouVersionContext);
24
24
 
25
25
  const languagesClient = useMemo(() => {
26
26
  if (!context?.appKey) {
27
27
  throw new Error(
28
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
28
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
29
29
  );
30
30
  }
31
31
  return new LanguagesClient(
package/src/usePassage.ts CHANGED
@@ -1,13 +1,13 @@
1
- "use client";
1
+ 'use client';
2
2
 
3
- import { useBibleClient } from "./useBibleClient";
4
- import { useApiData, type UseApiDataOptions } from "./useApiData";
5
- import type { BiblePassage } from "@youversion/platform-core";
3
+ import { useBibleClient } from './useBibleClient';
4
+ import { useApiData, type UseApiDataOptions } from './useApiData';
5
+ import type { BiblePassage } from '@youversion/platform-core';
6
6
 
7
7
  type usePassageProps = {
8
8
  versionId: number;
9
9
  usfm: string;
10
- format?: "html" | "text";
10
+ format?: 'html' | 'text';
11
11
  include_headings?: boolean;
12
12
  include_notes?: boolean;
13
13
  options?: UseApiDataOptions;
@@ -16,7 +16,7 @@ type usePassageProps = {
16
16
  export function usePassage({
17
17
  versionId,
18
18
  usfm,
19
- format = "html",
19
+ format = 'html',
20
20
  include_headings = false,
21
21
  include_notes = false,
22
22
  options,
@@ -29,17 +29,10 @@ export function usePassage({
29
29
  const bibleClient = useBibleClient();
30
30
 
31
31
  // Don't attempt to fetch if usfm is invalid
32
- const isValidUsfm = Boolean(usfm) && usfm !== "undefined" && usfm !== "null";
32
+ const isValidUsfm = Boolean(usfm) && usfm !== 'undefined' && usfm !== 'null';
33
33
 
34
34
  const { data, loading, error, refetch } = useApiData<BiblePassage>(
35
- () =>
36
- bibleClient.getPassage(
37
- versionId,
38
- usfm,
39
- format,
40
- include_headings,
41
- include_notes,
42
- ),
35
+ () => bibleClient.getPassage(versionId, usfm, format, include_headings, include_notes),
43
36
  [bibleClient, versionId, usfm, format, include_headings, include_notes],
44
37
  { enabled: options?.enabled !== false && isValidUsfm },
45
38
  );
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useVerseOfTheDay } from './useVOTD';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import { BibleClient, ApiClient, type VOTD } from '@youversion/platform-core';
7
7
 
8
8
  // Mock the core package
@@ -31,7 +31,7 @@ describe('useVerseOfTheDay', () => {
31
31
 
32
32
  const createWrapper = (contextValue: { appKey: string }) => {
33
33
  return ({ children }: { children: ReactNode }) => (
34
- <BibleSDKContext.Provider value={contextValue}>{children}</BibleSDKContext.Provider>
34
+ <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
35
35
  );
36
36
  };
37
37
 
@@ -56,7 +56,7 @@ describe('useVerseOfTheDay', () => {
56
56
  describe('context validation', () => {
57
57
  it('should throw error when context is not provided', () => {
58
58
  expect(() => renderHook(() => useVerseOfTheDay(1))).toThrow(
59
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
59
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
60
60
  );
61
61
  });
62
62
 
@@ -66,7 +66,7 @@ describe('useVerseOfTheDay', () => {
66
66
  });
67
67
 
68
68
  expect(() => renderHook(() => useVerseOfTheDay(1), { wrapper })).toThrow(
69
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
69
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
70
70
  );
71
71
  });
72
72
  });
@@ -103,13 +103,13 @@ describe('useVerseOfTheDay', () => {
103
103
  let currentAppKey = mockAppKey;
104
104
 
105
105
  const wrapper = ({ children }: { children: ReactNode }) => (
106
- <BibleSDKContext.Provider
106
+ <YouVersionContext.Provider
107
107
  value={{
108
108
  appKey: currentAppKey,
109
109
  }}
110
110
  >
111
111
  {children}
112
- </BibleSDKContext.Provider>
112
+ </YouVersionContext.Provider>
113
113
  );
114
114
 
115
115
  const { rerender } = renderHook(() => useVerseOfTheDay(1), { wrapper });
@@ -0,0 +1,378 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-argument */
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { renderHook, act } from '@testing-library/react';
4
+ import {
5
+ YouVersionAPIUsers,
6
+ YouVersionPlatformConfiguration,
7
+ SignInWithYouVersionPermission,
8
+ } from '@youversion/platform-core';
9
+ import { useYVAuth } from './useYVAuth';
10
+ import { YouVersionAuthContext } from './context/YouVersionAuthContext';
11
+ import { createMockUserInfo, createMockAuthResult } from './__tests__/mocks/auth';
12
+ import { createAuthProviderWrapper } from './__tests__/utils/test-utils';
13
+
14
+ // Mock the core modules
15
+ vi.mock('@youversion/platform-core', () => {
16
+ // Create a mock configuration object that can be updated
17
+ const mockConfiguration = {
18
+ accessToken: null as string | null,
19
+ idToken: null as string | null,
20
+ refreshToken: null as string | null,
21
+ appKey: '',
22
+ apiHost: 'test-api.example.com',
23
+ installationId: null as string | null,
24
+ clearAuthTokens: vi.fn(() => {
25
+ mockConfiguration.accessToken = null;
26
+ mockConfiguration.idToken = null;
27
+ mockConfiguration.refreshToken = null;
28
+ }),
29
+ saveAuthData: vi.fn(
30
+ (
31
+ accessToken: string | null,
32
+ refreshToken: string | null,
33
+ idToken: string | null,
34
+ installationId: string | null,
35
+ ) => {
36
+ mockConfiguration.accessToken = accessToken;
37
+ mockConfiguration.refreshToken = refreshToken;
38
+ mockConfiguration.idToken = idToken;
39
+ mockConfiguration.installationId = installationId;
40
+ },
41
+ ),
42
+ };
43
+
44
+ return {
45
+ YouVersionAPIUsers: {
46
+ signIn: vi.fn(),
47
+ handleAuthCallback: vi.fn(),
48
+ userInfo: vi.fn(),
49
+ refreshTokenIfNeeded: vi.fn(),
50
+ },
51
+ YouVersionPlatformConfiguration: mockConfiguration,
52
+ SignInWithYouVersionPermission: {
53
+ bibles: 'bibles',
54
+ highlights: 'highlights',
55
+ user: 'user',
56
+ },
57
+ YouVersionUserInfo: class YouVersionUserInfo {
58
+ readonly name?: string;
59
+ readonly userId?: string;
60
+ readonly email?: string;
61
+ readonly avatarUrlFormat?: string;
62
+
63
+ constructor(data: any) {
64
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
65
+ this.name = data.name;
66
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
67
+ this.userId = data.id;
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
69
+ this.email = data.email;
70
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
71
+ this.avatarUrlFormat = data.avatar_url;
72
+ }
73
+
74
+ getAvatarUrl(width: number = 200, height: number = 200): URL | null {
75
+ if (!this.avatarUrlFormat) {
76
+ return null;
77
+ }
78
+ try {
79
+ let urlString = this.avatarUrlFormat;
80
+ urlString = urlString.replace('{width}', width.toString());
81
+ urlString = urlString.replace('{height}', height.toString());
82
+ return new URL(urlString);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ get avatarUrl(): URL | null {
89
+ return this.getAvatarUrl();
90
+ }
91
+ },
92
+ SignInWithYouVersionResult: class SignInWithYouVersionResult {
93
+ accessToken: string | undefined;
94
+ expiryDate: Date | undefined;
95
+ refreshToken: string | undefined;
96
+ idToken: string | undefined;
97
+ permissions: string[] | undefined;
98
+ yvpUserId: string | undefined;
99
+ name: string | undefined;
100
+ profilePicture: string | undefined;
101
+ email: string | undefined;
102
+
103
+ constructor(props: {
104
+ accessToken?: string;
105
+ expiresIn?: number;
106
+ refreshToken?: string;
107
+ idToken?: string;
108
+ permissions?: string[];
109
+ yvpUserId?: string;
110
+ name?: string;
111
+ profilePicture?: string;
112
+ email?: string;
113
+ }) {
114
+ this.accessToken = props.accessToken;
115
+ this.expiryDate = props.expiresIn
116
+ ? new Date(Date.now() + props.expiresIn * 1000)
117
+ : new Date();
118
+ this.refreshToken = props.refreshToken;
119
+ this.idToken = props.idToken;
120
+ this.permissions = props.permissions;
121
+ this.yvpUserId = props.yvpUserId;
122
+ this.name = props.name;
123
+ this.profilePicture = props.profilePicture;
124
+ this.email = props.email;
125
+ }
126
+ },
127
+ };
128
+ });
129
+
130
+ const mockUserInfo = createMockUserInfo();
131
+ const mockAuthResult = createMockAuthResult();
132
+
133
+ // Mock window object
134
+ const mockWindow = {
135
+ location: {
136
+ href: 'https://example.com',
137
+ search: '',
138
+ },
139
+ };
140
+
141
+ const TestWrapper = createAuthProviderWrapper();
142
+
143
+ // Helper function to render hook and wait for it to be ready
144
+ const renderAuthHook = async () => {
145
+ const hookResult = renderHook(() => useYVAuth(), {
146
+ wrapper: TestWrapper,
147
+ });
148
+
149
+ // Wait for the lazy-loaded provider with act to handle suspended data
150
+ await act(async () => {
151
+ await new Promise((resolve) => setTimeout(resolve, 200));
152
+ });
153
+
154
+ return hookResult;
155
+ };
156
+
157
+ describe('useYVAuth', () => {
158
+ beforeEach(() => {
159
+ vi.clearAllMocks();
160
+
161
+ // Setup window mock
162
+ vi.stubGlobal('window', mockWindow);
163
+ mockWindow.location.search = '';
164
+
165
+ // Reset configuration mocks
166
+ YouVersionPlatformConfiguration.clearAuthTokens();
167
+ YouVersionPlatformConfiguration.installationId = null;
168
+ });
169
+
170
+ afterEach(() => {
171
+ vi.clearAllMocks();
172
+ vi.unstubAllGlobals();
173
+ });
174
+
175
+ describe('initialization', () => {
176
+ it('should return unauthenticated state when no user info available', async () => {
177
+ const { result } = await renderAuthHook();
178
+
179
+ // Add a small extra wait if still null
180
+ if (result.current === null) {
181
+ await new Promise((resolve) => setTimeout(resolve, 200));
182
+ }
183
+
184
+ expect(result.current).not.toBeNull();
185
+ expect(result.current.auth.isAuthenticated).toBe(false);
186
+ expect(result.current.auth.accessToken).toBe(null);
187
+ expect(result.current.auth.idToken).toBe(null);
188
+ expect(result.current.userInfo).toBe(null);
189
+ });
190
+
191
+ it('should throw error when used outside provider', () => {
192
+ expect(() => {
193
+ renderHook(() => useYVAuth());
194
+ }).toThrow('useYouVersionAuthContext must be used within an auth provider');
195
+ });
196
+ });
197
+
198
+ describe('signIn', () => {
199
+ it('should call YouVersionAPIUsers.signIn with correct parameters', async () => {
200
+ const { result } = await renderAuthHook();
201
+ const redirectUrl = 'https://example.com/callback';
202
+ const permissions = [SignInWithYouVersionPermission.bibles];
203
+
204
+ await act(async () => {
205
+ await result.current.signIn({ redirectUrl, permissions });
206
+ });
207
+
208
+ expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(
209
+ new Set(permissions),
210
+ redirectUrl,
211
+ );
212
+ });
213
+
214
+ it('should call signIn with empty permissions when not provided', async () => {
215
+ const { result } = await renderAuthHook();
216
+ const redirectUrl = 'https://example.com/callback';
217
+
218
+ await act(async () => {
219
+ await result.current.signIn({ redirectUrl });
220
+ });
221
+
222
+ expect(vi.mocked(YouVersionAPIUsers.signIn)).toHaveBeenCalledWith(new Set([]), redirectUrl);
223
+ });
224
+
225
+ it('should throw error when signIn fails', async () => {
226
+ const { result } = await renderAuthHook();
227
+ const error = new Error('Sign in failed');
228
+ vi.mocked(YouVersionAPIUsers.signIn).mockRejectedValue(error);
229
+
230
+ await expect(
231
+ act(async () => {
232
+ await result.current.signIn({ redirectUrl: 'https://example.com/callback' });
233
+ }),
234
+ ).rejects.toThrow('Sign in failed');
235
+ });
236
+ });
237
+
238
+ describe('processCallback', () => {
239
+ it('should call handleAuthCallback and return result', async () => {
240
+ const { result } = await renderAuthHook();
241
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
242
+
243
+ let callbackResult;
244
+ await act(async () => {
245
+ callbackResult = await result.current.processCallback();
246
+ });
247
+
248
+ expect(vi.mocked(YouVersionAPIUsers.handleAuthCallback)).toHaveBeenCalled();
249
+ expect(callbackResult).toEqual(mockAuthResult);
250
+ });
251
+
252
+ it('should throw error when callback processing fails', async () => {
253
+ const { result } = await renderAuthHook();
254
+ const error = new Error('Callback processing failed');
255
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockRejectedValue(error);
256
+
257
+ await expect(
258
+ act(async () => {
259
+ await result.current.processCallback();
260
+ }),
261
+ ).rejects.toThrow('Callback processing failed');
262
+ });
263
+
264
+ it('should return null when no result from callback', async () => {
265
+ const { result } = await renderAuthHook();
266
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(null);
267
+
268
+ let callbackResult;
269
+ await act(async () => {
270
+ callbackResult = await result.current.processCallback();
271
+ });
272
+
273
+ expect(callbackResult).toBe(null);
274
+ });
275
+ });
276
+
277
+ describe('signOut', () => {
278
+ it('should call clearAuthTokens and reset user info', async () => {
279
+ const { result } = await renderAuthHook();
280
+
281
+ act(() => {
282
+ result.current.signOut();
283
+ });
284
+
285
+ expect(YouVersionPlatformConfiguration.clearAuthTokens).toHaveBeenCalled();
286
+ });
287
+ });
288
+
289
+ describe('auth state', () => {
290
+ it('should derive correct auth state from configuration', async () => {
291
+ YouVersionPlatformConfiguration.saveAuthData('access-token', null, 'id-token', null);
292
+ const { result } = await renderAuthHook();
293
+
294
+ expect(result.current.auth.accessToken).toBe('access-token');
295
+ expect(result.current.auth.idToken).toBe('id-token');
296
+ });
297
+ });
298
+
299
+ describe('memoization', () => {
300
+ it('should memoize auth state when values do not change', async () => {
301
+ const { result, rerender } = await renderAuthHook();
302
+ const firstAuthState = result.current.auth;
303
+ rerender();
304
+
305
+ expect(result.current.auth).toBe(firstAuthState);
306
+ });
307
+
308
+ it('should create new auth state when context values change', () => {
309
+ // Test with multiple renders to verify different context values create new auth states
310
+ const { result: result1 } = renderHook(() => useYVAuth(), {
311
+ wrapper: ({ children }) => (
312
+ <YouVersionAuthContext.Provider
313
+ value={{
314
+ userInfo: null,
315
+ setUserInfo: () => {
316
+ // Mock function for testing
317
+ },
318
+ isLoading: false,
319
+ error: null,
320
+ }}
321
+ >
322
+ {children}
323
+ </YouVersionAuthContext.Provider>
324
+ ),
325
+ });
326
+
327
+ const firstAuthState = result1.current.auth;
328
+ expect(result1.current.auth.isAuthenticated).toBe(false);
329
+ expect(result1.current.userInfo).toBe(null);
330
+
331
+ // Create a new hook instance with different context values
332
+ const { result: result2 } = renderHook(() => useYVAuth(), {
333
+ wrapper: ({ children }) => (
334
+ <YouVersionAuthContext.Provider
335
+ value={{
336
+ userInfo: mockUserInfo,
337
+ setUserInfo: () => {
338
+ // Mock function for testing
339
+ },
340
+ isLoading: false,
341
+ error: null,
342
+ }}
343
+ >
344
+ {children}
345
+ </YouVersionAuthContext.Provider>
346
+ ),
347
+ });
348
+
349
+ // Verify that userInfo changed and auth state is different
350
+ expect(result2.current.userInfo).toEqual(mockUserInfo);
351
+ expect(result2.current.auth.isAuthenticated).toBe(true);
352
+ expect(result2.current.auth).not.toBe(firstAuthState);
353
+ });
354
+
355
+ it('should memoize callbacks', async () => {
356
+ const { result, rerender } = await renderAuthHook();
357
+ const firstSignIn = result.current.signIn;
358
+ const firstSignOut = result.current.signOut;
359
+ const firstProcessCallback = result.current.processCallback;
360
+
361
+ rerender();
362
+
363
+ expect(result.current.signIn).toBe(firstSignIn);
364
+ expect(result.current.signOut).toBe(firstSignOut);
365
+ expect(result.current.processCallback).toBe(firstProcessCallback);
366
+ });
367
+ });
368
+
369
+ describe('error handling', () => {
370
+ it('should include error in auth state when provider has error', async () => {
371
+ const { result } = await renderAuthHook();
372
+ // This test would require modifying the TestWrapper to inject an error
373
+ // For now, we'll test the structure
374
+ expect(result.current.auth).toHaveProperty('error');
375
+ expect(result.current.auth.result).toBe(null);
376
+ });
377
+ });
378
+ });