@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +56 -0
- package/README.md +3 -3
- package/dist/__tests__/mocks/auth.d.ts +12 -0
- package/dist/__tests__/mocks/auth.d.ts.map +1 -0
- package/dist/__tests__/mocks/auth.js +44 -0
- package/dist/__tests__/mocks/auth.js.map +1 -0
- package/dist/__tests__/utils/test-utils.d.ts +15 -0
- package/dist/__tests__/utils/test-utils.d.ts.map +1 -0
- package/dist/__tests__/utils/test-utils.js +24 -0
- package/dist/__tests__/utils/test-utils.js.map +1 -0
- package/dist/context/YouVersionAuthContext.d.ts +4 -0
- package/dist/context/YouVersionAuthContext.d.ts.map +1 -0
- package/dist/context/YouVersionAuthContext.js +13 -0
- package/dist/context/YouVersionAuthContext.js.map +1 -0
- package/dist/context/YouVersionAuthProvider.d.ts +8 -0
- package/dist/context/YouVersionAuthProvider.d.ts.map +1 -0
- package/dist/context/YouVersionAuthProvider.js +80 -0
- package/dist/context/YouVersionAuthProvider.js.map +1 -0
- package/dist/context/YouVersionContext.d.ts +8 -0
- package/dist/context/YouVersionContext.d.ts.map +1 -0
- package/dist/context/YouVersionContext.js +4 -0
- package/dist/context/YouVersionContext.js.map +1 -0
- package/dist/context/YouVersionProvider.d.ts +17 -0
- package/dist/context/YouVersionProvider.d.ts.map +1 -0
- package/dist/context/YouVersionProvider.js +25 -0
- package/dist/context/YouVersionProvider.js.map +1 -0
- package/dist/context/index.d.ts +2 -2
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +2 -2
- package/dist/context/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/types/auth.d.ts +18 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/auth.js.map +1 -0
- package/dist/useBibleClient.js +3 -3
- package/dist/useBibleClient.js.map +1 -1
- package/dist/useHighlights.js +3 -3
- package/dist/useHighlights.js.map +1 -1
- package/dist/useLanguages.js +3 -3
- package/dist/useLanguages.js.map +1 -1
- package/dist/usePassage.d.ts +3 -3
- package/dist/usePassage.d.ts.map +1 -1
- package/dist/usePassage.js +5 -5
- package/dist/usePassage.js.map +1 -1
- package/dist/useYVAuth.d.ts +97 -0
- package/dist/useYVAuth.d.ts.map +1 -0
- package/dist/useYVAuth.js +135 -0
- package/dist/useYVAuth.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/mocks/auth.ts +48 -0
- package/src/__tests__/utils/test-utils.tsx +43 -0
- package/src/context/YouVersionAuthContext.test.tsx +88 -0
- package/src/context/YouVersionAuthContext.tsx +20 -0
- package/src/context/YouVersionAuthProvider.test.tsx +373 -0
- package/src/context/YouVersionAuthProvider.tsx +90 -0
- package/src/context/{BibleSDKContext.tsx → YouVersionContext.tsx} +2 -2
- package/src/context/YouVersionProvider.tsx +65 -0
- package/src/context/index.ts +2 -2
- package/src/index.ts +4 -0
- package/src/types/auth.ts +20 -0
- package/src/useBibleClient.test.tsx +14 -14
- package/src/useBibleClient.ts +3 -3
- package/src/useHighlights.test.tsx +6 -6
- package/src/useHighlights.ts +3 -3
- package/src/useLanguages.test.tsx +6 -6
- package/src/useLanguages.ts +3 -3
- package/src/usePassage.ts +8 -15
- package/src/useVOTD.test.tsx +6 -6
- package/src/useYVAuth.test.tsx +378 -0
- package/src/useYVAuth.ts +179 -0
- package/dist/context/BibleSDKContext.d.ts +0 -8
- package/dist/context/BibleSDKContext.d.ts.map +0 -1
- package/dist/context/BibleSDKContext.js +0 -4
- package/dist/context/BibleSDKContext.js.map +0 -1
- package/dist/context/BibleSDKProvider.d.ts +0 -9
- package/dist/context/BibleSDKProvider.d.ts.map +0 -1
- package/dist/context/BibleSDKProvider.js +0 -18
- package/dist/context/BibleSDKProvider.js.map +0 -1
- package/src/context/BibleSDKProvider.tsx +0 -35
package/src/useBibleClient.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useContext, useMemo } from 'react';
|
|
4
|
-
import {
|
|
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(
|
|
8
|
+
const context = useContext(YouVersionContext);
|
|
9
9
|
|
|
10
10
|
return useMemo(() => {
|
|
11
11
|
if (!context?.appKey) {
|
|
12
12
|
throw new Error(
|
|
13
|
-
'
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
<
|
|
134
|
+
<YouVersionContext.Provider
|
|
135
135
|
value={{
|
|
136
136
|
appKey: currentAppKey,
|
|
137
137
|
}}
|
|
138
138
|
>
|
|
139
139
|
{children}
|
|
140
|
-
</
|
|
140
|
+
</YouVersionContext.Provider>
|
|
141
141
|
);
|
|
142
142
|
|
|
143
143
|
const { rerender } = renderHook(() => useHighlights(), { wrapper });
|
package/src/useHighlights.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useCallback } from 'react';
|
|
4
4
|
import { useContext } from 'react';
|
|
5
|
-
import {
|
|
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(
|
|
27
|
+
const context = useContext(YouVersionContext);
|
|
28
28
|
|
|
29
29
|
const highlightsClient = useMemo(() => {
|
|
30
30
|
if (!context?.appKey) {
|
|
31
31
|
throw new Error(
|
|
32
|
-
'
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
<
|
|
149
|
+
<YouVersionContext.Provider
|
|
150
150
|
value={{
|
|
151
151
|
appKey: currentAppKey,
|
|
152
152
|
}}
|
|
153
153
|
>
|
|
154
154
|
{children}
|
|
155
|
-
</
|
|
155
|
+
</YouVersionContext.Provider>
|
|
156
156
|
);
|
|
157
157
|
|
|
158
158
|
const { rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
|
package/src/useLanguages.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import { useContext } from 'react';
|
|
5
|
-
import {
|
|
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(
|
|
23
|
+
const context = useContext(YouVersionContext);
|
|
24
24
|
|
|
25
25
|
const languagesClient = useMemo(() => {
|
|
26
26
|
if (!context?.appKey) {
|
|
27
27
|
throw new Error(
|
|
28
|
-
'
|
|
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
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
import { useBibleClient } from
|
|
4
|
-
import { useApiData, type UseApiDataOptions } from
|
|
5
|
-
import type { BiblePassage } from
|
|
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?:
|
|
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 =
|
|
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 !==
|
|
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
|
);
|
package/src/useVOTD.test.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
<
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
<
|
|
106
|
+
<YouVersionContext.Provider
|
|
107
107
|
value={{
|
|
108
108
|
appKey: currentAppKey,
|
|
109
109
|
}}
|
|
110
110
|
>
|
|
111
111
|
{children}
|
|
112
|
-
</
|
|
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
|
+
});
|