@youversion/platform-react-hooks 0.5.8 → 0.7.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 +49 -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 +4 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -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 +5 -5
  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 +6 -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
@@ -0,0 +1,373 @@
1
+ /* eslint-disable @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-argument */
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import { YouVersionAPIUsers, YouVersionPlatformConfiguration } from '@youversion/platform-core';
5
+ import YouVersionAuthProvider from './YouVersionAuthProvider';
6
+ import { useYouVersionAuthContext } from './YouVersionAuthContext';
7
+ import type { AuthConfig } from '../types/auth';
8
+ import { createMockUserInfo, createMockAuthResult } from '../__tests__/mocks/auth';
9
+
10
+ // Mock the core modules
11
+ vi.mock('@youversion/platform-core', () => {
12
+ let mockInstallationId = 'auto-generated-installation-id';
13
+ let mockIdToken: string | null = null;
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
+ permissions: string[] | undefined;
95
+ yvpUserId: string | undefined;
96
+ name: string | undefined;
97
+ profilePicture: string | undefined;
98
+ email: string | undefined;
99
+
100
+ constructor(props: {
101
+ accessToken?: string;
102
+ expiresIn?: number;
103
+ refreshToken?: string;
104
+ idToken?: string;
105
+ permissions?: string[];
106
+ yvpUserId?: string;
107
+ name?: string;
108
+ profilePicture?: string;
109
+ email?: string;
110
+ }) {
111
+ this.accessToken = props.accessToken;
112
+ this.expiryDate = props.expiresIn
113
+ ? new Date(Date.now() + props.expiresIn * 1000)
114
+ : new Date();
115
+ this.refreshToken = props.refreshToken;
116
+ this.idToken = props.idToken;
117
+ this.permissions = props.permissions;
118
+ this.yvpUserId = props.yvpUserId;
119
+ this.name = props.name;
120
+ this.profilePicture = props.profilePicture;
121
+ this.email = props.email;
122
+ }
123
+ },
124
+ };
125
+ });
126
+
127
+ const mockConfig: AuthConfig = {
128
+ appKey: 'test-app-key',
129
+ apiHost: 'test-api.example.com',
130
+ };
131
+
132
+ const mockUserInfo = createMockUserInfo();
133
+ const mockAuthResult = createMockAuthResult();
134
+
135
+ // Mock window and location
136
+ const mockWindow = {
137
+ location: {
138
+ href: 'https://example.com',
139
+ search: '',
140
+ },
141
+ };
142
+
143
+ // Test component to access context
144
+ function TestChild() {
145
+ const { userInfo, isLoading, error } = useYouVersionAuthContext();
146
+
147
+ return (
148
+ <div>
149
+ <div data-testid="user-info">{userInfo ? JSON.stringify(userInfo) : 'null'}</div>
150
+ <div data-testid="is-loading">{isLoading.toString()}</div>
151
+ <div data-testid="error">{error ? error.message : 'null'}</div>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ describe('YouVersionAuthProvider', () => {
157
+ beforeEach(() => {
158
+ vi.clearAllMocks();
159
+
160
+ // Setup window mock
161
+ vi.stubGlobal('window', mockWindow);
162
+ mockWindow.location.search = '';
163
+
164
+ // Reset configuration
165
+ YouVersionPlatformConfiguration.appKey = '';
166
+ YouVersionPlatformConfiguration.apiHost = 'test-api.example.com';
167
+ YouVersionPlatformConfiguration.clearAuthTokens();
168
+ });
169
+
170
+ afterEach(() => {
171
+ vi.clearAllMocks();
172
+ vi.unstubAllGlobals();
173
+ });
174
+
175
+ describe('initialization', () => {
176
+ it('should configure YouVersionPlatformConfiguration on mount', async () => {
177
+ render(
178
+ <YouVersionAuthProvider config={mockConfig}>
179
+ <TestChild />
180
+ </YouVersionAuthProvider>,
181
+ );
182
+
183
+ // Wait for async initialization to complete
184
+ await vi.waitFor(() => {
185
+ expect(YouVersionPlatformConfiguration.appKey).toBe(mockConfig.appKey);
186
+ expect(YouVersionPlatformConfiguration.apiHost).toBe(mockConfig.apiHost);
187
+ });
188
+ });
189
+
190
+ it('should use default apiHost when not provided', async () => {
191
+ const configWithoutApiHost = {
192
+ appKey: 'test-app-key',
193
+ installationId: 'test-installation-id',
194
+ };
195
+
196
+ render(
197
+ <YouVersionAuthProvider config={configWithoutApiHost}>
198
+ <TestChild />
199
+ </YouVersionAuthProvider>,
200
+ );
201
+
202
+ await vi.waitFor(() => {
203
+ expect(YouVersionPlatformConfiguration.appKey).toBe('test-app-key');
204
+ expect(YouVersionPlatformConfiguration.installationId).toBeTruthy();
205
+ // Since config had no apiHost, component should set default (in real implementation this would be 'api.youversion.com')
206
+ // But since we're mocking, we can test that it gets set to something defined
207
+ expect(YouVersionPlatformConfiguration.apiHost).toBeTruthy();
208
+ });
209
+ });
210
+
211
+ it('should handle null installationId', async () => {
212
+ const configWithoutInstallation = {
213
+ appKey: 'test-app-key',
214
+ };
215
+
216
+ render(
217
+ <YouVersionAuthProvider config={configWithoutInstallation}>
218
+ <TestChild />
219
+ </YouVersionAuthProvider>,
220
+ );
221
+
222
+ await vi.waitFor(() => {
223
+ expect(YouVersionPlatformConfiguration.installationId).not.toBe(null);
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('OAuth callback handling', () => {
229
+ it('should detect OAuth callback with state parameter', async () => {
230
+ mockWindow.location.search = '?state=test-state&code=auth-code';
231
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
232
+ vi.mocked(YouVersionAPIUsers.userInfo).mockReturnValue(mockUserInfo as any);
233
+
234
+ // Mock the configuration to return the id token after handleAuthCallback
235
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockImplementation(() => {
236
+ YouVersionPlatformConfiguration.saveAuthData(null, null, 'test-id-token', null);
237
+ return Promise.resolve(mockAuthResult as any);
238
+ });
239
+
240
+ const { getByTestId } = render(
241
+ <YouVersionAuthProvider config={mockConfig}>
242
+ <TestChild />
243
+ </YouVersionAuthProvider>,
244
+ );
245
+
246
+ await vi.waitFor(() => {
247
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
248
+ });
249
+
250
+ expect(vi.mocked(YouVersionAPIUsers).handleAuthCallback).toHaveBeenCalled();
251
+ expect(vi.mocked(YouVersionAPIUsers).userInfo).toHaveBeenCalledWith('test-id-token');
252
+ expect(getByTestId('user-info')).toHaveTextContent(JSON.stringify(mockUserInfo));
253
+ });
254
+
255
+ it('should detect OAuth callback with error parameter', async () => {
256
+ mockWindow.location.search = '?error=access_denied&error_description=User+denied+access';
257
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
258
+
259
+ const { getByTestId } = render(
260
+ <YouVersionAuthProvider config={mockConfig}>
261
+ <TestChild />
262
+ </YouVersionAuthProvider>,
263
+ );
264
+
265
+ await vi.waitFor(() => {
266
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
267
+ });
268
+
269
+ expect(vi.mocked(YouVersionAPIUsers).handleAuthCallback).toHaveBeenCalled();
270
+ });
271
+
272
+ it('should handle callback error and set error state', async () => {
273
+ mockWindow.location.search = '?state=test-state&code=auth-code';
274
+ const callbackError = new Error('Callback processing failed');
275
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockRejectedValue(callbackError);
276
+
277
+ const { getByTestId } = render(
278
+ <YouVersionAuthProvider config={mockConfig}>
279
+ <TestChild />
280
+ </YouVersionAuthProvider>,
281
+ );
282
+
283
+ await vi.waitFor(() => {
284
+ expect(getByTestId('error')).toHaveTextContent('Callback processing failed');
285
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
286
+ expect(getByTestId('user-info')).toHaveTextContent('null');
287
+ });
288
+ });
289
+
290
+ it('should handle callback with no idToken', async () => {
291
+ mockWindow.location.search = '?state=test-state&code=auth-code';
292
+ vi.mocked(YouVersionAPIUsers.handleAuthCallback).mockResolvedValue(mockAuthResult as any);
293
+ YouVersionPlatformConfiguration.saveAuthData(null, null, null, null);
294
+
295
+ const { getByTestId } = render(
296
+ <YouVersionAuthProvider config={mockConfig}>
297
+ <TestChild />
298
+ </YouVersionAuthProvider>,
299
+ );
300
+
301
+ await vi.waitFor(() => {
302
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
303
+ });
304
+
305
+ expect(vi.mocked(YouVersionAPIUsers).userInfo).not.toHaveBeenCalled();
306
+ expect(getByTestId('user-info')).toHaveTextContent('null');
307
+ });
308
+ });
309
+
310
+ describe('existing token handling', () => {
311
+ it('should refresh token when refresh token exists', async () => {
312
+ // Set up refresh token before mounting component
313
+ YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
314
+
315
+ // Mock refreshTokenIfNeeded to set the id token after successful refresh
316
+ vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockImplementation(() => {
317
+ YouVersionPlatformConfiguration.saveAuthData(null, null, 'refreshed-id-token', null);
318
+ return Promise.resolve(true);
319
+ });
320
+ vi.mocked(YouVersionAPIUsers.userInfo).mockReturnValue(mockUserInfo as any);
321
+
322
+ const { getByTestId } = render(
323
+ <YouVersionAuthProvider config={mockConfig}>
324
+ <TestChild />
325
+ </YouVersionAuthProvider>,
326
+ );
327
+
328
+ await vi.waitFor(() => {
329
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
330
+ });
331
+
332
+ expect(vi.mocked(YouVersionAPIUsers).refreshTokenIfNeeded).toHaveBeenCalled();
333
+ expect(vi.mocked(YouVersionAPIUsers).userInfo).toHaveBeenCalledWith('refreshed-id-token');
334
+ expect(getByTestId('user-info')).toHaveTextContent(JSON.stringify(mockUserInfo));
335
+ });
336
+
337
+ it('should handle refresh token failure', async () => {
338
+ YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
339
+ vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockRejectedValue(
340
+ new Error('Refresh failed'),
341
+ );
342
+
343
+ const { getByTestId } = render(
344
+ <YouVersionAuthProvider config={mockConfig}>
345
+ <TestChild />
346
+ </YouVersionAuthProvider>,
347
+ );
348
+
349
+ await vi.waitFor(() => {
350
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
351
+ });
352
+
353
+ expect(getByTestId('user-info')).toHaveTextContent('null');
354
+ });
355
+
356
+ it('should clear user when refresh token exists but no idToken after refresh', async () => {
357
+ YouVersionPlatformConfiguration.saveAuthData(null, 'existing-refresh-token', null, null);
358
+ vi.mocked(YouVersionAPIUsers.refreshTokenIfNeeded).mockResolvedValue(false);
359
+
360
+ const { getByTestId } = render(
361
+ <YouVersionAuthProvider config={mockConfig}>
362
+ <TestChild />
363
+ </YouVersionAuthProvider>,
364
+ );
365
+
366
+ await vi.waitFor(() => {
367
+ expect(getByTestId('is-loading')).toHaveTextContent('false');
368
+ });
369
+
370
+ expect(getByTestId('user-info')).toHaveTextContent('null');
371
+ });
372
+ });
373
+ });
@@ -0,0 +1,90 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState, type ReactNode } from 'react';
4
+ import {
5
+ YouVersionAPIUsers,
6
+ YouVersionPlatformConfiguration,
7
+ type YouVersionUserInfo,
8
+ } from '@youversion/platform-core';
9
+ import { YouVersionAuthContext } from './YouVersionAuthContext';
10
+ import type { AuthConfig, AuthContextValue } from '../types/auth';
11
+
12
+ export interface YouVersionAuthProviderProps {
13
+ config: AuthConfig;
14
+ children: ReactNode;
15
+ }
16
+
17
+ export default function YouVersionAuthProvider({
18
+ config,
19
+ children,
20
+ }: YouVersionAuthProviderProps): React.ReactElement {
21
+ const [userInfo, setUserInfo] = useState<YouVersionUserInfo | null>(null);
22
+ const [isLoading, setIsLoading] = useState(true);
23
+ const [error, setError] = useState<Error | null>(null);
24
+
25
+ useEffect(() => {
26
+ let mounted = true;
27
+ const initAuth = async () => {
28
+ // Set configuration
29
+ YouVersionPlatformConfiguration.appKey = config.appKey;
30
+ YouVersionPlatformConfiguration.apiHost = config.apiHost ?? 'api.youversion.com';
31
+
32
+ if (typeof window !== 'undefined') {
33
+ // Check for OAuth callback
34
+ const urlParams = new URLSearchParams(window.location.search);
35
+ const isOAuthCallback = urlParams.has('state') || urlParams.has('error');
36
+
37
+ if (isOAuthCallback) {
38
+ try {
39
+ const result = await YouVersionAPIUsers.handleAuthCallback();
40
+ if (result && YouVersionPlatformConfiguration.idToken) {
41
+ const info = YouVersionAPIUsers.userInfo(YouVersionPlatformConfiguration.idToken);
42
+ if (!mounted) return;
43
+ setUserInfo(info);
44
+ }
45
+ } catch (err) {
46
+ if (!mounted) return;
47
+ setError(err as Error);
48
+ }
49
+ } else {
50
+ // Check for existing token
51
+ const refreshToken = YouVersionPlatformConfiguration.refreshToken;
52
+ if (refreshToken) {
53
+ try {
54
+ await YouVersionAPIUsers.refreshTokenIfNeeded();
55
+ const idToken = YouVersionPlatformConfiguration.idToken;
56
+ if (idToken) {
57
+ const info = YouVersionAPIUsers.userInfo(idToken);
58
+ if (!mounted) return;
59
+ setUserInfo(info);
60
+ } else {
61
+ if (!mounted) return;
62
+ setUserInfo(null);
63
+ }
64
+ } catch {
65
+ if (!mounted) return;
66
+ setUserInfo(null);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ if (!mounted) return;
73
+ setIsLoading(false);
74
+ };
75
+
76
+ void initAuth();
77
+ return () => {
78
+ mounted = false;
79
+ };
80
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
81
+
82
+ const value: AuthContextValue = {
83
+ userInfo,
84
+ setUserInfo,
85
+ isLoading,
86
+ error,
87
+ };
88
+
89
+ return <YouVersionAuthContext.Provider value={value}>{children}</YouVersionAuthContext.Provider>;
90
+ }
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { createContext } from 'react';
4
4
 
5
- type BibleSDKContextData = {
5
+ type YouVersionContextData = {
6
6
  appKey: string;
7
7
  apiHost?: string;
8
8
  installationId?: string;
9
9
  };
10
10
 
11
- export const BibleSDKContext = createContext<BibleSDKContextData | null>(null);
11
+ export const YouVersionContext = createContext<YouVersionContextData | null>(null);
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import type { PropsWithChildren, ReactNode } from 'react';
4
+ import { lazy, Suspense, useEffect } from 'react';
5
+ import { YouVersionContext } from './YouVersionContext';
6
+ import { YouVersionPlatformConfiguration } from '@youversion/platform-core';
7
+
8
+ interface YouVersionProviderPropsBase {
9
+ children: ReactNode;
10
+ appKey: string;
11
+ apiHost?: string;
12
+ }
13
+
14
+ interface YouVersionProviderPropsWithAuth extends YouVersionProviderPropsBase {
15
+ authRedirectUrl: string;
16
+ includeAuth: true;
17
+ }
18
+
19
+ interface YouVersionProviderPropsWithoutAuth extends YouVersionProviderPropsBase {
20
+ includeAuth?: false;
21
+ authRedirectUrl?: never;
22
+ }
23
+
24
+ const AuthProvider = lazy(() => import('./YouVersionAuthProvider'));
25
+
26
+ export function YouVersionProvider(
27
+ props: PropsWithChildren<YouVersionProviderPropsWithAuth | YouVersionProviderPropsWithoutAuth>,
28
+ ): React.ReactElement {
29
+ const { appKey, apiHost = 'api.youversion.com', includeAuth, children } = props;
30
+
31
+ // Syncing appKey and apiHost to YouVersionPlatformConfiguration
32
+ // so that this can be in sync with any other code that uses
33
+ // the YouVersionPlatformConfiguration, of which a lot of our
34
+ // core package uses this configuration.
35
+ useEffect(() => {
36
+ YouVersionPlatformConfiguration.appKey = appKey;
37
+ YouVersionPlatformConfiguration.apiHost = apiHost;
38
+ }, [appKey, apiHost]);
39
+
40
+ if (includeAuth) {
41
+ const { authRedirectUrl } = props;
42
+
43
+ // Installation ID gets set automatically by YouVersionPlatformConfiguration
44
+ return (
45
+ <YouVersionContext.Provider
46
+ value={{ appKey, apiHost, installationId: YouVersionPlatformConfiguration.installationId }}
47
+ >
48
+ <Suspense>
49
+ <AuthProvider config={{ appKey, apiHost, redirectUri: authRedirectUrl }}>
50
+ {children}
51
+ </AuthProvider>
52
+ </Suspense>
53
+ </YouVersionContext.Provider>
54
+ );
55
+ }
56
+
57
+ // Installation ID gets set automatically by YouVersionPlatformConfiguration
58
+ return (
59
+ <YouVersionContext.Provider
60
+ value={{ appKey, apiHost, installationId: YouVersionPlatformConfiguration.installationId }}
61
+ >
62
+ {children}
63
+ </YouVersionContext.Provider>
64
+ );
65
+ }
@@ -1,5 +1,5 @@
1
- export * from './BibleSDKContext';
2
- export * from './BibleSDKProvider';
1
+ export * from './YouVersionContext';
2
+ export * from './YouVersionProvider';
3
3
  export * from './ReaderContext';
4
4
  export * from './ReaderProvider';
5
5
  export * from './VerseSelectionProvider';
package/src/index.ts CHANGED
@@ -18,3 +18,9 @@ export * from './usePassage';
18
18
  export * from './useVOTD';
19
19
  export * from './useHighlights';
20
20
  export * from './useLanguages';
21
+
22
+ // Auth hooks
23
+ export * from './useYVAuth';
24
+ export * from './context/YouVersionAuthContext';
25
+ export * from './context/YouVersionAuthProvider';
26
+ export * from './types/auth';
@@ -0,0 +1,20 @@
1
+ import type { YouVersionUserInfo } from '@youversion/platform-core';
2
+ import type { Dispatch, SetStateAction } from 'react';
3
+
4
+ export interface AuthConfig {
5
+ appKey: string;
6
+ apiHost?: string;
7
+ redirectUri?: string;
8
+ }
9
+
10
+ export interface AuthContextValue {
11
+ userInfo: YouVersionUserInfo | null;
12
+ setUserInfo: Dispatch<SetStateAction<YouVersionUserInfo | null>>;
13
+ isLoading: boolean;
14
+ error: Error | null;
15
+ }
16
+
17
+ export interface AuthProviderProps {
18
+ config: AuthConfig;
19
+ children: React.ReactNode;
20
+ }
@@ -2,7 +2,7 @@ import { renderHook } from '@testing-library/react';
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useBibleClient } from './useBibleClient';
5
- import { BibleSDKContext } from './context';
5
+ import { YouVersionContext } from './context';
6
6
  import { BibleClient, ApiClient } from '@youversion/platform-core';
7
7
 
8
8
  vi.mock('@youversion/platform-core', async () => {
@@ -31,13 +31,13 @@ describe('useBibleClient', () => {
31
31
 
32
32
  it('should create and return a BibleClient instance when context is valid', () => {
33
33
  const wrapper = ({ children }: { children: ReactNode }) => (
34
- <BibleSDKContext.Provider
34
+ <YouVersionContext.Provider
35
35
  value={{
36
36
  appKey: mockAppKey,
37
37
  }}
38
38
  >
39
39
  {children}
40
- </BibleSDKContext.Provider>
40
+ </YouVersionContext.Provider>
41
41
  );
42
42
 
43
43
  const { result } = renderHook(() => useBibleClient(), { wrapper });
@@ -51,35 +51,35 @@ describe('useBibleClient', () => {
51
51
 
52
52
  it('should throw error when context is not provided', () => {
53
53
  expect(() => renderHook(() => useBibleClient())).toThrow(
54
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
54
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
55
55
  );
56
56
  });
57
57
 
58
58
  it('should throw error when appKey is missing', () => {
59
59
  const wrapper = ({ children }: { children: ReactNode }) => (
60
- <BibleSDKContext.Provider
60
+ <YouVersionContext.Provider
61
61
  value={{
62
62
  appKey: '',
63
63
  }}
64
64
  >
65
65
  {children}
66
- </BibleSDKContext.Provider>
66
+ </YouVersionContext.Provider>
67
67
  );
68
68
 
69
69
  expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
70
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
70
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
71
71
  );
72
72
  });
73
73
 
74
74
  it('should memoize the BibleClient instance', () => {
75
75
  const wrapper = ({ children }: { children: ReactNode }) => (
76
- <BibleSDKContext.Provider
76
+ <YouVersionContext.Provider
77
77
  value={{
78
78
  appKey: mockAppKey,
79
79
  }}
80
80
  >
81
81
  {children}
82
- </BibleSDKContext.Provider>
82
+ </YouVersionContext.Provider>
83
83
  );
84
84
 
85
85
  const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
@@ -96,13 +96,13 @@ describe('useBibleClient', () => {
96
96
  let currentAppKey = mockAppKey;
97
97
 
98
98
  const wrapper = ({ children }: { children: ReactNode }) => (
99
- <BibleSDKContext.Provider
99
+ <YouVersionContext.Provider
100
100
  value={{
101
101
  appKey: currentAppKey,
102
102
  }}
103
103
  >
104
104
  {children}
105
- </BibleSDKContext.Provider>
105
+ </YouVersionContext.Provider>
106
106
  );
107
107
 
108
108
  const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
@@ -127,17 +127,17 @@ describe('useBibleClient', () => {
127
127
 
128
128
  it('should throw error when appKey is null', () => {
129
129
  const wrapper = ({ children }: { children: ReactNode }) => (
130
- <BibleSDKContext.Provider
130
+ <YouVersionContext.Provider
131
131
  value={{
132
132
  appKey: null as unknown as string,
133
133
  }}
134
134
  >
135
135
  {children}
136
- </BibleSDKContext.Provider>
136
+ </YouVersionContext.Provider>
137
137
  );
138
138
 
139
139
  expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
140
- 'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
140
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
141
141
  );
142
142
  });
143
143
  });