@youversion/platform-react-hooks 1.18.1 → 1.20.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/AGENTS.md +3 -7
- package/CHANGELOG.md +32 -0
- package/dist/__tests__/mocks/bibles.d.ts +6 -1
- package/dist/__tests__/mocks/bibles.d.ts.map +1 -1
- package/dist/__tests__/mocks/bibles.js +10 -0
- package/dist/__tests__/mocks/bibles.js.map +1 -1
- package/dist/__tests__/mocks/core-mock-factory.d.ts +83 -0
- package/dist/__tests__/mocks/core-mock-factory.d.ts.map +1 -0
- package/dist/__tests__/mocks/core-mock-factory.js +138 -0
- package/dist/__tests__/mocks/core-mock-factory.js.map +1 -0
- package/dist/context/ReaderContext.d.ts +6 -0
- package/dist/context/ReaderContext.d.ts.map +1 -1
- package/dist/context/ReaderContext.js +6 -0
- package/dist/context/ReaderContext.js.map +1 -1
- package/dist/context/ReaderProvider.d.ts +3 -0
- package/dist/context/ReaderProvider.d.ts.map +1 -1
- package/dist/context/ReaderProvider.js +3 -0
- package/dist/context/ReaderProvider.js.map +1 -1
- package/dist/context/VerseSelectionContext.d.ts +6 -0
- package/dist/context/VerseSelectionContext.d.ts.map +1 -1
- package/dist/context/VerseSelectionContext.js +3 -0
- package/dist/context/VerseSelectionContext.js.map +1 -1
- package/dist/context/VerseSelectionProvider.d.ts +3 -0
- package/dist/context/VerseSelectionProvider.d.ts.map +1 -1
- package/dist/context/VerseSelectionProvider.js +3 -0
- package/dist/context/VerseSelectionProvider.js.map +1 -1
- package/dist/test/utils.d.ts +7 -0
- package/dist/test/utils.d.ts.map +1 -0
- package/dist/test/utils.js +7 -0
- package/dist/test/utils.js.map +1 -0
- package/dist/useChapterNavigation.d.ts +3 -0
- package/dist/useChapterNavigation.d.ts.map +1 -1
- package/dist/useChapterNavigation.js +3 -0
- package/dist/useChapterNavigation.js.map +1 -1
- package/dist/useInitData.d.ts +4 -0
- package/dist/useInitData.d.ts.map +1 -1
- package/dist/useInitData.js +4 -0
- package/dist/useInitData.js.map +1 -1
- package/dist/useVerseSelection.d.ts +3 -0
- package/dist/useVerseSelection.d.ts.map +1 -1
- package/dist/useVerseSelection.js +3 -0
- package/dist/useVerseSelection.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/mocks/bibles.ts +18 -1
- package/src/__tests__/mocks/core-mock-factory.ts +226 -0
- package/src/context/ReaderContext.tsx +6 -0
- package/src/context/ReaderProvider.tsx +3 -0
- package/src/context/VerseSelectionContext.tsx +6 -0
- package/src/context/VerseSelectionProvider.tsx +3 -0
- package/src/context/YouVersionAuthProvider.test.tsx +14 -131
- package/src/test/utils.tsx +12 -0
- package/src/useBibleClient.test.tsx +8 -37
- package/src/useBook.test.tsx +158 -0
- package/src/useBooks.test.tsx +148 -0
- package/src/useChapter.test.tsx +70 -128
- package/src/useChapterNavigation.ts +3 -0
- package/src/useChapters.test.tsx +80 -150
- package/src/useHighlights.test.tsx +33 -104
- package/src/useInitData.ts +4 -0
- package/src/useLanguage.test.tsx +8 -10
- package/src/useLanguageClient.test.tsx +9 -25
- package/src/useLanguages.test.tsx +27 -64
- package/src/usePassage.test.tsx +304 -0
- package/src/useTheme.test.tsx +32 -0
- package/src/useVOTD.test.tsx +28 -67
- package/src/useVerse.test.tsx +73 -149
- package/src/useVerseSelection.ts +3 -0
- package/src/useVerses.test.tsx +37 -104
- package/src/useVersion.test.tsx +29 -66
- package/src/useVersions.test.tsx +72 -154
- package/src/useYVAuth.test.tsx +26 -134
- package/src/utility/getDayOfYear.test.ts +48 -0
- package/vitest.config.ts +12 -0
- package/src/context/ReaderProvider.test.tsx +0 -264
- package/src/context/VerseSelectionProvider.test.tsx +0 -362
- package/src/useChapterNavigation.test.tsx +0 -160
- package/src/useVerseSelection.test.tsx +0 -33
- package/vitest.setup.ts +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVerseSelection.d.ts","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAEzC,wBAAgB,iBAAiB,IAAI,yBAAyB,CAM7D"}
|
|
1
|
+
{"version":3,"file":"useVerseSelection.d.ts","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,yBAAyB,CAM7D"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
2
|
import { VerseSelectionContext, } from './context/VerseSelectionContext';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
5
|
+
*/
|
|
3
6
|
export function useVerseSelection() {
|
|
4
7
|
const context = useContext(VerseSelectionContext);
|
|
5
8
|
if (!context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useVerseSelection.js","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EACL,qBAAqB,GAEtB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
1
|
+
{"version":3,"file":"useVerseSelection.js","sourceRoot":"","sources":["../src/useVerseSelection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EACL,qBAAqB,GAEtB,MAAM,iCAAiC,CAAC;AAEzC;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,OAAO,GAAG,UAAU,CAAC,qBAAqB,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youversion/platform-react-hooks",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@youversion/platform-core": "1.
|
|
25
|
+
"@youversion/platform-core": "1.20.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": ">=19.1.0 <20.0.0"
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BibleBook,
|
|
3
|
+
BibleChapter,
|
|
4
|
+
BiblePassage,
|
|
5
|
+
BibleVerse,
|
|
6
|
+
BibleVersion,
|
|
7
|
+
} from '@youversion/platform-core';
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Creates a mock Bible version for testing
|
|
@@ -54,3 +60,14 @@ export const createMockVerse = (overrides?: Partial<BibleVerse>): BibleVerse =>
|
|
|
54
60
|
title: '1',
|
|
55
61
|
...overrides,
|
|
56
62
|
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a mock Bible passage for testing
|
|
66
|
+
* @param overrides Optional properties to override defaults
|
|
67
|
+
*/
|
|
68
|
+
export const createMockPassage = (overrides?: Partial<BiblePassage>): BiblePassage => ({
|
|
69
|
+
id: 'JHN.3.16',
|
|
70
|
+
content: '<p>For God so loved the world...</p>',
|
|
71
|
+
reference: 'John 3:16',
|
|
72
|
+
...overrides,
|
|
73
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { vi, type MockedFunction } from 'vitest';
|
|
2
|
+
import type { YouVersionAPIUsers } from '@youversion/platform-core';
|
|
3
|
+
|
|
4
|
+
interface MockUserInfoData {
|
|
5
|
+
name?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
avatar_url?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface MockAuthResultProps {
|
|
12
|
+
accessToken?: string;
|
|
13
|
+
expiresIn?: number;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
idToken?: string;
|
|
16
|
+
yvpUserId?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
profilePicture?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MockYouVersionUserInfo {
|
|
23
|
+
readonly name?: string;
|
|
24
|
+
readonly userId?: string;
|
|
25
|
+
readonly email?: string;
|
|
26
|
+
readonly avatarUrlFormat?: string;
|
|
27
|
+
|
|
28
|
+
constructor(data: MockUserInfoData) {
|
|
29
|
+
this.name = data.name;
|
|
30
|
+
this.userId = data.id;
|
|
31
|
+
this.email = data.email;
|
|
32
|
+
this.avatarUrlFormat = data.avatar_url;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getAvatarUrl(width: number = 200, height: number = 200): URL | null {
|
|
36
|
+
if (!this.avatarUrlFormat) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
let urlString = this.avatarUrlFormat;
|
|
41
|
+
if (urlString.startsWith('//')) {
|
|
42
|
+
urlString = 'https:' + urlString;
|
|
43
|
+
}
|
|
44
|
+
urlString = urlString.replace('{width}', width.toString());
|
|
45
|
+
urlString = urlString.replace('{height}', height.toString());
|
|
46
|
+
return new URL(urlString);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get avatarUrl(): URL | null {
|
|
53
|
+
return this.getAvatarUrl();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class MockSignInWithYouVersionResult {
|
|
58
|
+
readonly accessToken: string | undefined;
|
|
59
|
+
readonly expiryDate: Date | undefined;
|
|
60
|
+
readonly refreshToken: string | undefined;
|
|
61
|
+
readonly idToken: string | undefined;
|
|
62
|
+
readonly yvpUserId: string | undefined;
|
|
63
|
+
readonly name: string | undefined;
|
|
64
|
+
readonly profilePicture: string | undefined;
|
|
65
|
+
readonly email: string | undefined;
|
|
66
|
+
|
|
67
|
+
constructor(props: MockAuthResultProps) {
|
|
68
|
+
this.accessToken = props.accessToken;
|
|
69
|
+
this.expiryDate = props.expiresIn ? new Date(Date.now() + props.expiresIn * 1000) : new Date();
|
|
70
|
+
this.refreshToken = props.refreshToken;
|
|
71
|
+
this.idToken = props.idToken;
|
|
72
|
+
this.yvpUserId = props.yvpUserId;
|
|
73
|
+
this.name = props.name;
|
|
74
|
+
this.profilePicture = props.profilePicture;
|
|
75
|
+
this.email = props.email;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface MockConfiguration {
|
|
80
|
+
accessToken: string | null;
|
|
81
|
+
idToken: string | null;
|
|
82
|
+
refreshToken: string | null;
|
|
83
|
+
appKey: string;
|
|
84
|
+
apiHost: string;
|
|
85
|
+
installationId: string | null;
|
|
86
|
+
clearAuthTokens: MockedFunction<() => void>;
|
|
87
|
+
saveAuthData: MockedFunction<
|
|
88
|
+
(
|
|
89
|
+
accessToken: string | null,
|
|
90
|
+
refreshToken: string | null,
|
|
91
|
+
idToken: string | null,
|
|
92
|
+
installationId: string | null,
|
|
93
|
+
) => void
|
|
94
|
+
>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SimpleCoreMockFactory {
|
|
98
|
+
YouVersionAPIUsers: {
|
|
99
|
+
signIn: MockedFunction<typeof YouVersionAPIUsers.signIn>;
|
|
100
|
+
handleAuthCallback: MockedFunction<typeof YouVersionAPIUsers.handleAuthCallback>;
|
|
101
|
+
userInfo: MockedFunction<typeof YouVersionAPIUsers.userInfo>;
|
|
102
|
+
refreshTokenIfNeeded: MockedFunction<typeof YouVersionAPIUsers.refreshTokenIfNeeded>;
|
|
103
|
+
};
|
|
104
|
+
YouVersionPlatformConfiguration: MockConfiguration;
|
|
105
|
+
SignInWithYouVersionPermission: Record<string, string>;
|
|
106
|
+
YouVersionUserInfo: typeof MockYouVersionUserInfo;
|
|
107
|
+
SignInWithYouVersionResult: typeof MockSignInWithYouVersionResult;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface GetterCoreMockFactory {
|
|
111
|
+
YouVersionAPIUsers: {
|
|
112
|
+
handleAuthCallback: MockedFunction<typeof YouVersionAPIUsers.handleAuthCallback>;
|
|
113
|
+
userInfo: MockedFunction<typeof YouVersionAPIUsers.userInfo>;
|
|
114
|
+
refreshTokenIfNeeded: MockedFunction<typeof YouVersionAPIUsers.refreshTokenIfNeeded>;
|
|
115
|
+
};
|
|
116
|
+
YouVersionPlatformConfiguration: {
|
|
117
|
+
appKey: string;
|
|
118
|
+
installationId: string;
|
|
119
|
+
apiHost: string;
|
|
120
|
+
readonly idToken: string | null;
|
|
121
|
+
readonly refreshToken: string | null;
|
|
122
|
+
readonly accessToken: string | null;
|
|
123
|
+
clearAuthTokens: MockedFunction<() => void>;
|
|
124
|
+
saveAuthData: MockedFunction<
|
|
125
|
+
(accessToken: string | null, refreshToken: string | null, idToken: string | null) => void
|
|
126
|
+
>;
|
|
127
|
+
};
|
|
128
|
+
YouVersionUserInfo: typeof MockYouVersionUserInfo;
|
|
129
|
+
SignInWithYouVersionResult: typeof MockSignInWithYouVersionResult;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createSimpleCoreMockFactory(): SimpleCoreMockFactory {
|
|
133
|
+
const mockConfiguration: MockConfiguration = {
|
|
134
|
+
accessToken: null,
|
|
135
|
+
idToken: null,
|
|
136
|
+
refreshToken: null,
|
|
137
|
+
appKey: '',
|
|
138
|
+
apiHost: 'test-api.example.com',
|
|
139
|
+
installationId: null,
|
|
140
|
+
clearAuthTokens: vi.fn(() => {
|
|
141
|
+
mockConfiguration.accessToken = null;
|
|
142
|
+
mockConfiguration.idToken = null;
|
|
143
|
+
mockConfiguration.refreshToken = null;
|
|
144
|
+
}),
|
|
145
|
+
saveAuthData: vi.fn(
|
|
146
|
+
(
|
|
147
|
+
accessToken: string | null,
|
|
148
|
+
refreshToken: string | null,
|
|
149
|
+
idToken: string | null,
|
|
150
|
+
installationId: string | null,
|
|
151
|
+
) => {
|
|
152
|
+
mockConfiguration.accessToken = accessToken;
|
|
153
|
+
mockConfiguration.refreshToken = refreshToken;
|
|
154
|
+
mockConfiguration.idToken = idToken;
|
|
155
|
+
mockConfiguration.installationId = installationId;
|
|
156
|
+
},
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
YouVersionAPIUsers: {
|
|
162
|
+
signIn: vi.fn(),
|
|
163
|
+
handleAuthCallback: vi.fn(),
|
|
164
|
+
userInfo: vi.fn(),
|
|
165
|
+
refreshTokenIfNeeded: vi.fn(),
|
|
166
|
+
},
|
|
167
|
+
YouVersionPlatformConfiguration: mockConfiguration,
|
|
168
|
+
SignInWithYouVersionPermission: {
|
|
169
|
+
bibles: 'bibles',
|
|
170
|
+
highlights: 'highlights',
|
|
171
|
+
votd: 'votd',
|
|
172
|
+
demographics: 'demographics',
|
|
173
|
+
bibleActivity: 'bible_activity',
|
|
174
|
+
},
|
|
175
|
+
YouVersionUserInfo: MockYouVersionUserInfo,
|
|
176
|
+
SignInWithYouVersionResult: MockSignInWithYouVersionResult,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createGetterCoreMockFactory(): GetterCoreMockFactory {
|
|
181
|
+
let mockInstallationId = 'auto-generated-installation-id';
|
|
182
|
+
let mockIdToken: string | null = null;
|
|
183
|
+
let mockRefreshToken: string | null = null;
|
|
184
|
+
let mockAccessToken: string | null = null;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
YouVersionAPIUsers: {
|
|
188
|
+
handleAuthCallback: vi.fn(),
|
|
189
|
+
userInfo: vi.fn(),
|
|
190
|
+
refreshTokenIfNeeded: vi.fn(),
|
|
191
|
+
},
|
|
192
|
+
YouVersionPlatformConfiguration: {
|
|
193
|
+
appKey: '',
|
|
194
|
+
get installationId() {
|
|
195
|
+
return mockInstallationId;
|
|
196
|
+
},
|
|
197
|
+
set installationId(value) {
|
|
198
|
+
if (value) mockInstallationId = value;
|
|
199
|
+
},
|
|
200
|
+
apiHost: 'test-api.example.com',
|
|
201
|
+
get idToken() {
|
|
202
|
+
return mockIdToken;
|
|
203
|
+
},
|
|
204
|
+
get refreshToken() {
|
|
205
|
+
return mockRefreshToken;
|
|
206
|
+
},
|
|
207
|
+
get accessToken() {
|
|
208
|
+
return mockAccessToken;
|
|
209
|
+
},
|
|
210
|
+
clearAuthTokens: vi.fn(() => {
|
|
211
|
+
mockIdToken = null;
|
|
212
|
+
mockRefreshToken = null;
|
|
213
|
+
mockAccessToken = null;
|
|
214
|
+
}),
|
|
215
|
+
saveAuthData: vi.fn(
|
|
216
|
+
(accessToken: string | null, refreshToken: string | null, idToken: string | null) => {
|
|
217
|
+
mockAccessToken = accessToken;
|
|
218
|
+
mockRefreshToken = refreshToken;
|
|
219
|
+
mockIdToken = idToken;
|
|
220
|
+
},
|
|
221
|
+
),
|
|
222
|
+
},
|
|
223
|
+
YouVersionUserInfo: MockYouVersionUserInfo,
|
|
224
|
+
SignInWithYouVersionResult: MockSignInWithYouVersionResult,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -14,8 +14,14 @@ type ReaderContextData = {
|
|
|
14
14
|
setVerse: (verse: BibleVerse | null) => void;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
19
|
+
*/
|
|
17
20
|
export const ReaderContext = createContext<ReaderContextData | null>(null);
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
24
|
+
*/
|
|
19
25
|
export function useReaderContext(): ReaderContextData {
|
|
20
26
|
const context = useContext(ReaderContext);
|
|
21
27
|
|
|
@@ -11,6 +11,9 @@ type ReaderProviderProps = {
|
|
|
11
11
|
currentVerse: BibleVerse | null;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
16
|
+
*/
|
|
14
17
|
export function ReaderProvider(props: PropsWithChildren<ReaderProviderProps>): React.ReactElement {
|
|
15
18
|
const [currentVersion, setCurrentVersion] = useState<BibleVersion>(props.currentVersion);
|
|
16
19
|
const [currentBook, setCurrentBook] = useState<BibleBook>(props.currentBook);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
5
|
+
*/
|
|
3
6
|
export type VerseSelectionContextData = {
|
|
4
7
|
selectedVerseUsfms: Set<string>;
|
|
5
8
|
toggleVerse: (usfm: string) => void;
|
|
@@ -8,4 +11,7 @@ export type VerseSelectionContextData = {
|
|
|
8
11
|
selectedCount: number;
|
|
9
12
|
};
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
16
|
+
*/
|
|
11
17
|
export const VerseSelectionContext = createContext<VerseSelectionContextData | null>(null);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type PropsWithChildren, useCallback, useState } from 'react';
|
|
2
2
|
import { VerseSelectionContext } from './VerseSelectionContext';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated No replacement needed. Remove usage. Will be removed in the next major version.
|
|
6
|
+
*/
|
|
4
7
|
export function VerseSelectionProvider({ children }: PropsWithChildren): React.ReactElement {
|
|
5
8
|
const [selectedVerseUsfms, setSelectedVerseUsfms] = useState<Set<string>>(new Set());
|
|
6
9
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
2
|
import { render } from '@testing-library/react';
|
|
4
3
|
import { YouVersionAPIUsers, YouVersionPlatformConfiguration } from '@youversion/platform-core';
|
|
5
4
|
import YouVersionAuthProvider from './YouVersionAuthProvider';
|
|
@@ -7,118 +6,10 @@ import { useYouVersionAuthContext } from './YouVersionAuthContext';
|
|
|
7
6
|
import type { AuthConfig } from '../types/auth';
|
|
8
7
|
import { createMockUserInfo, createMockAuthResult } from '../__tests__/mocks/auth';
|
|
9
8
|
|
|
10
|
-
// Mock the core modules
|
|
11
|
-
vi.mock('@youversion/platform-core', () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
let mockRefreshToken: string | null = null;
|
|
15
|
-
let mockAccessToken: string | null = null;
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
YouVersionAPIUsers: {
|
|
19
|
-
handleAuthCallback: vi.fn(),
|
|
20
|
-
userInfo: vi.fn(),
|
|
21
|
-
refreshTokenIfNeeded: vi.fn(),
|
|
22
|
-
},
|
|
23
|
-
YouVersionPlatformConfiguration: {
|
|
24
|
-
appKey: '',
|
|
25
|
-
get installationId() {
|
|
26
|
-
return mockInstallationId;
|
|
27
|
-
},
|
|
28
|
-
set installationId(value) {
|
|
29
|
-
if (value) mockInstallationId = value;
|
|
30
|
-
},
|
|
31
|
-
apiHost: 'test-api.example.com',
|
|
32
|
-
get idToken() {
|
|
33
|
-
return mockIdToken;
|
|
34
|
-
},
|
|
35
|
-
get refreshToken() {
|
|
36
|
-
return mockRefreshToken;
|
|
37
|
-
},
|
|
38
|
-
get accessToken() {
|
|
39
|
-
return mockAccessToken;
|
|
40
|
-
},
|
|
41
|
-
clearAuthTokens: vi.fn(() => {
|
|
42
|
-
mockIdToken = null;
|
|
43
|
-
mockRefreshToken = null;
|
|
44
|
-
mockAccessToken = null;
|
|
45
|
-
}),
|
|
46
|
-
saveAuthData: vi.fn(
|
|
47
|
-
(accessToken: string | null, refreshToken: string | null, idToken: string | null) => {
|
|
48
|
-
mockAccessToken = accessToken;
|
|
49
|
-
mockRefreshToken = refreshToken;
|
|
50
|
-
mockIdToken = idToken;
|
|
51
|
-
},
|
|
52
|
-
),
|
|
53
|
-
},
|
|
54
|
-
YouVersionUserInfo: class YouVersionUserInfo {
|
|
55
|
-
readonly name?: string;
|
|
56
|
-
readonly userId?: string;
|
|
57
|
-
readonly email?: string;
|
|
58
|
-
readonly avatarUrlFormat?: string;
|
|
59
|
-
|
|
60
|
-
constructor(data: any) {
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
62
|
-
this.name = data.name;
|
|
63
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
64
|
-
this.userId = data.id;
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
66
|
-
this.email = data.email;
|
|
67
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
|
68
|
-
this.avatarUrlFormat = data.avatar_url;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
getAvatarUrl(width: number = 200, height: number = 200): URL | null {
|
|
72
|
-
if (!this.avatarUrlFormat) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
let urlString = this.avatarUrlFormat;
|
|
77
|
-
urlString = urlString.replace('{width}', width.toString());
|
|
78
|
-
urlString = urlString.replace('{height}', height.toString());
|
|
79
|
-
return new URL(urlString);
|
|
80
|
-
} catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get avatarUrl(): URL | null {
|
|
86
|
-
return this.getAvatarUrl();
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
SignInWithYouVersionResult: class SignInWithYouVersionResult {
|
|
90
|
-
accessToken: string | undefined;
|
|
91
|
-
expiryDate: Date | undefined;
|
|
92
|
-
refreshToken: string | undefined;
|
|
93
|
-
idToken: string | undefined;
|
|
94
|
-
yvpUserId: string | undefined;
|
|
95
|
-
name: string | undefined;
|
|
96
|
-
profilePicture: string | undefined;
|
|
97
|
-
email: string | undefined;
|
|
98
|
-
|
|
99
|
-
constructor(props: {
|
|
100
|
-
accessToken?: string;
|
|
101
|
-
expiresIn?: number;
|
|
102
|
-
refreshToken?: string;
|
|
103
|
-
idToken?: string;
|
|
104
|
-
yvpUserId?: string;
|
|
105
|
-
name?: string;
|
|
106
|
-
profilePicture?: string;
|
|
107
|
-
email?: string;
|
|
108
|
-
}) {
|
|
109
|
-
this.accessToken = props.accessToken;
|
|
110
|
-
this.expiryDate = props.expiresIn
|
|
111
|
-
? new Date(Date.now() + props.expiresIn * 1000)
|
|
112
|
-
: new Date();
|
|
113
|
-
this.refreshToken = props.refreshToken;
|
|
114
|
-
this.idToken = props.idToken;
|
|
115
|
-
this.yvpUserId = props.yvpUserId;
|
|
116
|
-
this.name = props.name;
|
|
117
|
-
this.profilePicture = props.profilePicture;
|
|
118
|
-
this.email = props.email;
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
};
|
|
9
|
+
// Mock the core modules using shared factory
|
|
10
|
+
vi.mock('@youversion/platform-core', async () => {
|
|
11
|
+
const { createGetterCoreMockFactory } = await import('../__tests__/mocks/core-mock-factory');
|
|
12
|
+
return createGetterCoreMockFactory();
|
|
122
13
|
});
|
|
123
14
|
|
|
124
15
|
const mockConfig: AuthConfig = {
|
|
@@ -152,8 +43,6 @@ function TestChild() {
|
|
|
152
43
|
|
|
153
44
|
describe('YouVersionAuthProvider', () => {
|
|
154
45
|
beforeEach(() => {
|
|
155
|
-
vi.clearAllMocks();
|
|
156
|
-
|
|
157
46
|
// Setup window mock
|
|
158
47
|
vi.stubGlobal('window', mockWindow);
|
|
159
48
|
mockWindow.location.search = '';
|
|
@@ -164,11 +53,6 @@ describe('YouVersionAuthProvider', () => {
|
|
|
164
53
|
YouVersionPlatformConfiguration.clearAuthTokens();
|
|
165
54
|
});
|
|
166
55
|
|
|
167
|
-
afterEach(() => {
|
|
168
|
-
vi.clearAllMocks();
|
|
169
|
-
vi.unstubAllGlobals();
|
|
170
|
-
});
|
|
171
|
-
|
|
172
56
|
describe('initialization', () => {
|
|
173
57
|
it('should configure YouVersionPlatformConfiguration on mount', async () => {
|
|
174
58
|
render(
|
|
@@ -225,11 +109,10 @@ describe('YouVersionAuthProvider', () => {
|
|
|
225
109
|
describe('OAuth callback handling', () => {
|
|
226
110
|
it('should detect OAuth callback with state parameter', async () => {
|
|
227
111
|
mockWindow.location.search = '?state=test-state&code=auth-code';
|
|
228
|
-
vi.
|
|
229
|
-
vi.mocked(YouVersionAPIUsers.userInfo).mockReturnValue(mockUserInfo as any);
|
|
112
|
+
vi.spyOn(YouVersionAPIUsers, 'userInfo').mockReturnValue(mockUserInfo);
|
|
230
113
|
|
|
231
114
|
// Mock the configuration to return the id token after handleAuthCallback
|
|
232
|
-
vi.
|
|
115
|
+
vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockImplementation(() => {
|
|
233
116
|
YouVersionPlatformConfiguration.saveAuthData(null, null, 'test-id-token', null);
|
|
234
117
|
return Promise.resolve(mockAuthResult as any);
|
|
235
118
|
});
|
|
@@ -251,7 +134,7 @@ describe('YouVersionAuthProvider', () => {
|
|
|
251
134
|
|
|
252
135
|
it('should detect OAuth callback with error parameter', async () => {
|
|
253
136
|
mockWindow.location.search = '?error=access_denied&error_description=User+denied+access';
|
|
254
|
-
vi.
|
|
137
|
+
vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockResolvedValue(mockAuthResult);
|
|
255
138
|
|
|
256
139
|
const { getByTestId } = render(
|
|
257
140
|
<YouVersionAuthProvider config={mockConfig}>
|
|
@@ -269,7 +152,7 @@ describe('YouVersionAuthProvider', () => {
|
|
|
269
152
|
it('should handle callback error and set error state', async () => {
|
|
270
153
|
mockWindow.location.search = '?state=test-state&code=auth-code';
|
|
271
154
|
const callbackError = new Error('Callback processing failed');
|
|
272
|
-
vi.
|
|
155
|
+
vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockRejectedValue(callbackError);
|
|
273
156
|
|
|
274
157
|
const { getByTestId } = render(
|
|
275
158
|
<YouVersionAuthProvider config={mockConfig}>
|
|
@@ -286,7 +169,7 @@ describe('YouVersionAuthProvider', () => {
|
|
|
286
169
|
|
|
287
170
|
it('should handle callback with no idToken', async () => {
|
|
288
171
|
mockWindow.location.search = '?state=test-state&code=auth-code';
|
|
289
|
-
vi.
|
|
172
|
+
vi.spyOn(YouVersionAPIUsers, 'handleAuthCallback').mockResolvedValue(mockAuthResult);
|
|
290
173
|
YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
|
|
291
174
|
|
|
292
175
|
const { getByTestId } = render(
|
|
@@ -310,11 +193,11 @@ describe('YouVersionAuthProvider', () => {
|
|
|
310
193
|
YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
|
|
311
194
|
|
|
312
195
|
// Mock refreshTokenIfNeeded to set the id token after successful refresh
|
|
313
|
-
vi.
|
|
196
|
+
vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockImplementation(() => {
|
|
314
197
|
YouVersionPlatformConfiguration.saveAuthData(null, null, 'refreshed-id-token', null);
|
|
315
198
|
return Promise.resolve(true);
|
|
316
199
|
});
|
|
317
|
-
vi.
|
|
200
|
+
vi.spyOn(YouVersionAPIUsers, 'userInfo').mockReturnValue(mockUserInfo);
|
|
318
201
|
|
|
319
202
|
const { getByTestId } = render(
|
|
320
203
|
<YouVersionAuthProvider config={mockConfig}>
|
|
@@ -333,7 +216,7 @@ describe('YouVersionAuthProvider', () => {
|
|
|
333
216
|
|
|
334
217
|
it('should handle refresh token failure', async () => {
|
|
335
218
|
YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
|
|
336
|
-
vi.
|
|
219
|
+
vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockRejectedValue(
|
|
337
220
|
new Error('Refresh failed'),
|
|
338
221
|
);
|
|
339
222
|
|
|
@@ -352,7 +235,7 @@ describe('YouVersionAuthProvider', () => {
|
|
|
352
235
|
|
|
353
236
|
it('should clear user when refresh token exists but no idToken after refresh', async () => {
|
|
354
237
|
YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
|
|
355
|
-
vi.
|
|
238
|
+
vi.spyOn(YouVersionAPIUsers, 'refreshTokenIfNeeded').mockResolvedValue(false);
|
|
356
239
|
|
|
357
240
|
const { getByTestId } = render(
|
|
358
241
|
<YouVersionAuthProvider config={mockConfig}>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode, ComponentType } from 'react';
|
|
2
|
+
import { YouVersionContext } from '../context';
|
|
3
|
+
|
|
4
|
+
export const createYVWrapper = (
|
|
5
|
+
appKey = 'test-app-key',
|
|
6
|
+
{ theme }: { theme?: 'light' | 'dark' } = {},
|
|
7
|
+
): ComponentType<{ children: ReactNode }> => {
|
|
8
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
9
|
+
<YouVersionContext.Provider value={{ appKey, theme }}>{children}</YouVersionContext.Provider>
|
|
10
|
+
);
|
|
11
|
+
return Wrapper;
|
|
12
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react';
|
|
2
|
-
import { describe,
|
|
2
|
+
import { describe, expect, vi, it } from 'vitest';
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
import { useBibleClient } from './useBibleClient';
|
|
5
5
|
import { YouVersionContext } from './context';
|
|
6
6
|
import { BibleClient, ApiClient } from '@youversion/platform-core';
|
|
7
|
+
import { createYVWrapper } from './test/utils';
|
|
7
8
|
|
|
8
9
|
vi.mock('@youversion/platform-core', async () => {
|
|
9
10
|
const actual = await vi.importActual('@youversion/platform-core');
|
|
@@ -23,27 +24,12 @@ vi.mock('@youversion/platform-core', async () => {
|
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
describe('useBibleClient', () => {
|
|
26
|
-
const mockAppKey = 'test-app-key';
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
vi.clearAllMocks();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
27
|
it('should create and return a BibleClient instance when context is valid', () => {
|
|
33
|
-
const wrapper = (
|
|
34
|
-
<YouVersionContext.Provider
|
|
35
|
-
value={{
|
|
36
|
-
appKey: mockAppKey,
|
|
37
|
-
}}
|
|
38
|
-
>
|
|
39
|
-
{children}
|
|
40
|
-
</YouVersionContext.Provider>
|
|
41
|
-
);
|
|
42
|
-
|
|
28
|
+
const wrapper = createYVWrapper();
|
|
43
29
|
const { result } = renderHook(() => useBibleClient(), { wrapper });
|
|
44
30
|
|
|
45
31
|
expect(ApiClient).toHaveBeenCalledWith({
|
|
46
|
-
appKey:
|
|
32
|
+
appKey: 'test-app-key',
|
|
47
33
|
});
|
|
48
34
|
expect(BibleClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
|
|
49
35
|
expect(result.current).toEqual(expect.objectContaining({ isBibleClient: true }));
|
|
@@ -57,13 +43,7 @@ describe('useBibleClient', () => {
|
|
|
57
43
|
|
|
58
44
|
it('should throw error when appKey is missing', () => {
|
|
59
45
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
60
|
-
<YouVersionContext.Provider
|
|
61
|
-
value={{
|
|
62
|
-
appKey: '',
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
{children}
|
|
66
|
-
</YouVersionContext.Provider>
|
|
46
|
+
<YouVersionContext.Provider value={{ appKey: '' }}>{children}</YouVersionContext.Provider>
|
|
67
47
|
);
|
|
68
48
|
|
|
69
49
|
expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
|
|
@@ -72,16 +52,7 @@ describe('useBibleClient', () => {
|
|
|
72
52
|
});
|
|
73
53
|
|
|
74
54
|
it('should memoize the BibleClient instance', () => {
|
|
75
|
-
const wrapper = (
|
|
76
|
-
<YouVersionContext.Provider
|
|
77
|
-
value={{
|
|
78
|
-
appKey: mockAppKey,
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
{children}
|
|
82
|
-
</YouVersionContext.Provider>
|
|
83
|
-
);
|
|
84
|
-
|
|
55
|
+
const wrapper = createYVWrapper();
|
|
85
56
|
const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
|
|
86
57
|
const firstClient = result.current;
|
|
87
58
|
|
|
@@ -93,7 +64,7 @@ describe('useBibleClient', () => {
|
|
|
93
64
|
});
|
|
94
65
|
|
|
95
66
|
it('should create new BibleClient when appKey changes', () => {
|
|
96
|
-
let currentAppKey =
|
|
67
|
+
let currentAppKey = 'test-app-key';
|
|
97
68
|
|
|
98
69
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
99
70
|
<YouVersionContext.Provider
|
|
@@ -110,7 +81,7 @@ describe('useBibleClient', () => {
|
|
|
110
81
|
const firstClient = result.current;
|
|
111
82
|
expect(BibleClient).toHaveBeenCalledTimes(1);
|
|
112
83
|
expect(ApiClient).toHaveBeenCalledWith({
|
|
113
|
-
appKey:
|
|
84
|
+
appKey: 'test-app-key',
|
|
114
85
|
});
|
|
115
86
|
|
|
116
87
|
currentAppKey = 'new-app-key';
|