@youversion/platform-react-hooks 1.8.0 → 1.9.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @youversion/platform-react-hooks@1.8.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/hooks
2
+ > @youversion/platform-react-hooks@1.9.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/hooks
3
3
  > tsc -p tsconfig.build.json
4
4
 
package/AGENTS.md ADDED
@@ -0,0 +1,183 @@
1
+ # @youversion/platform-react-hooks
2
+
3
+ ## OVERVIEW
4
+ React integration layer providing data fetching hooks with 3 core providers: YouVersionProvider, YouVersionAuthProvider, and ReaderProvider.
5
+
6
+ **Depends on `@youversion/platform-core` for all API calls.** Hooks delegate to core clients; do not implement raw HTTP here.
7
+
8
+ **Related packages:**
9
+ - For lower-level API clients → see `packages/core/AGENTS.md`
10
+ - For pre-built UI components → see `packages/ui/AGENTS.md`
11
+
12
+ ## STRUCTURE
13
+ - `use*.ts` - Data fetching hooks (useBook, useChapter, usePassage, useVersion, etc.)
14
+ - `context/` - Providers and contexts (separate files, exported via index.ts)
15
+ - `utility/` - Helper functions (useDebounce, extractTextFromHTML, extractVersesFromHTML)
16
+
17
+ ## PUBLIC API
18
+ - Data fetching hooks: useBook, useChapter, usePassage, useVersion, useVOTD, useVerse, useChapterNavigation, etc.
19
+ - YouVersionProvider - Core SDK configuration
20
+ - YouVersionAuthProvider - Authentication state
21
+ - ReaderProvider - Reading session context
22
+ - Utility functions exported from utility/index
23
+
24
+ ## PROVIDERS
25
+
26
+ - **YouVersionProvider**
27
+ - Holds core SDK configuration (API base URL, clients)
28
+ - Wrap this around your app before using any data hooks
29
+
30
+ - **YouVersionAuthProvider**
31
+ - Manages authentication state (userInfo, tokens, isLoading, error)
32
+ - Auth hooks like `useYVAuth` depend on this provider
33
+
34
+ - **ReaderProvider**
35
+ - Manages Bible reading session state (currentVersion, currentChapter, currentBook, currentVerse)
36
+ - Hooks like `useChapterNavigation` depend on this provider
37
+
38
+ ## DOs / DON'Ts
39
+
40
+ ✅ Do: Use `YouVersionProvider` for configuration and access that config in hooks
41
+ ✅ Do: Wrap async data access in hooks rather than calling core clients directly in components
42
+ ✅ Do: Keep hooks **UI-agnostic** (no JSX returned, no direct DOM manipulation)
43
+ ✅ Do: Use the `useApiData` pattern for new data fetching hooks
44
+
45
+ ❌ Don't: Import components from `@youversion/platform-react-ui`
46
+ ❌ Don't: Talk directly to `fetch`/HTTP; always use `@youversion/platform-core`
47
+ ❌ Don't: Access `window.localStorage` directly for auth; rely on core's storage abstractions
48
+
49
+ ## DATA FETCHING PATTERN
50
+
51
+ Hooks use a custom React Query-like pattern via `useApiData`:
52
+ - Returns `{ data, loading, error, refetch }`
53
+ - Provides caching and refetch capability
54
+ - New hooks should follow this same pattern
55
+
56
+ ## CONVENTIONS
57
+ - Context and Provider in separate files
58
+ - All contexts exported via context/index.ts
59
+ - TypeScript declarations generated separately (no bundling)
60
+ - Build: tsc only
61
+
62
+ ## USAGE EXAMPLES
63
+
64
+ ### Provider Setup (Required)
65
+
66
+ ```tsx
67
+ // Wrap your app with YouVersionProvider before using any hooks
68
+ import { YouVersionProvider } from '@youversion/platform-react-hooks';
69
+
70
+ function App() {
71
+ return (
72
+ <YouVersionProvider
73
+ appKey="your-app-key"
74
+ theme="light" // "light" | "dark"
75
+ >
76
+ <MyApp />
77
+ </YouVersionProvider>
78
+ );
79
+ }
80
+
81
+ // With authentication enabled
82
+ function AppWithAuth() {
83
+ return (
84
+ <YouVersionProvider
85
+ appKey="your-app-key"
86
+ includeAuth={true}
87
+ authRedirectUrl="https://myapp.com/callback"
88
+ >
89
+ <MyApp />
90
+ </YouVersionProvider>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ### Data Fetching Hooks
96
+
97
+ All data hooks return `{ data, loading, error, refetch }`:
98
+
99
+ ```tsx
100
+ import { useChapter, useVersion, useVerseOfTheDay } from '@youversion/platform-react-hooks';
101
+
102
+ // Fetch a Bible chapter
103
+ function ChapterView() {
104
+ const { chapter, loading, error } = useChapter(
105
+ 111, // versionId (e.g., 111 = NIV)
106
+ 'JHN', // book (USFM abbreviation)
107
+ 3 // chapter number
108
+ );
109
+
110
+ if (loading) return <div>Loading...</div>;
111
+ if (error) return <div>Error: {error.message}</div>;
112
+ return <div>{chapter?.content}</div>;
113
+ }
114
+
115
+ // Fetch Bible version metadata
116
+ function VersionInfo() {
117
+ const { version, loading } = useVersion(111);
118
+ if (loading) return <div>Loading...</div>;
119
+ return <div>{version?.name} ({version?.abbreviation})</div>;
120
+ }
121
+
122
+ // Fetch Verse of the Day
123
+ function DailyVerse() {
124
+ const dayOfYear = Math.floor((Date.now() - new Date(new Date().getFullYear(), 0, 0).getTime()) / 86400000);
125
+ const { data: votd, loading, refetch } = useVerseOfTheDay(dayOfYear);
126
+
127
+ if (loading) return <div>Loading...</div>;
128
+ return (
129
+ <div>
130
+ <p>{votd?.verse.text}</p>
131
+ <button onClick={refetch}>Refresh</button>
132
+ </div>
133
+ );
134
+ }
135
+ ```
136
+
137
+ ### Authentication Hook
138
+
139
+ ```tsx
140
+ import { useYVAuth } from '@youversion/platform-react-hooks';
141
+
142
+ function AuthExample() {
143
+ const { auth, userInfo, signIn, signOut } = useYVAuth();
144
+
145
+ if (auth.isLoading) return <div>Loading...</div>;
146
+
147
+ if (!auth.isAuthenticated) {
148
+ return (
149
+ <button onClick={() => signIn({ redirectUrl: window.location.origin + '/callback' })}>
150
+ Sign In with YouVersion
151
+ </button>
152
+ );
153
+ }
154
+
155
+ return (
156
+ <div>
157
+ <p>Welcome, {userInfo?.name}!</p>
158
+ <button onClick={signOut}>Sign Out</button>
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ### Conditional Fetching
165
+
166
+ ```tsx
167
+ // Use the `enabled` option to conditionally fetch
168
+ function ConditionalFetch({ versionId }: { versionId: number | null }) {
169
+ const { version, loading } = useVersion(versionId ?? 0, {
170
+ enabled: versionId !== null, // Only fetch when versionId is provided
171
+ });
172
+
173
+ // ...
174
+ }
175
+ ```
176
+
177
+ ## TESTING
178
+
179
+ - Run tests: `pnpm --filter @youversion/platform-react-hooks test`
180
+ - Framework: Vitest with jsdom environment
181
+ - React Testing Library for component/hook tests
182
+ - Mock object factories live in `__tests__/mocks` (not MSW - hooks delegate HTTP to core)
183
+ - Use provider wrappers for tests so hooks see the same context as in the app
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @youversion/platform-react-hooks
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d4b0071: feat(hooks): Add useLanguage hook to retrieve a language from api
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [d4b0071]
12
+ - @youversion/platform-core@1.9.0
13
+
14
+ ## 1.8.1
15
+
16
+ ### Patch Changes
17
+
18
+ - 607be3c: Refactor verse HTML transformation to support verse-level highlighting. Extract HTML processing logic to `verse-html-utils.ts` with new `wrapVerseContent()` function that wraps verse content in CSS-targetable `<span class="yv-v">` elements. Simplify footnote extraction using wrapped verse structure. Remove CSS rule preventing text wrapping. Add comprehensive test coverage for verse wrapping behavior.
19
+ - Updated dependencies [607be3c]
20
+ - @youversion/platform-core@1.8.1
21
+
3
22
  ## 1.8.0
4
23
 
5
24
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './usePassage';
18
18
  export * from './useVOTD';
19
19
  export * from './useHighlights';
20
20
  export * from './useLanguages';
21
+ export * from './useLanguage';
21
22
  export * from './useTheme';
22
23
  export * from './useYVAuth';
23
24
  export * from './types/auth';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAG3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAG3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './usePassage';
18
18
  export * from './useVOTD';
19
19
  export * from './useHighlights';
20
20
  export * from './useLanguages';
21
+ export * from './useLanguage';
21
22
  export * from './useTheme';
22
23
  // Auth hooks
23
24
  export * from './useYVAuth';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAE3B,aAAa;AACb,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAE3B,aAAa;AACb,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type UseApiDataOptions } from './useApiData';
2
+ import { type Language } from '@youversion/platform-core';
3
+ export declare function useLanguage(languageId: string, apiOptions?: UseApiDataOptions): {
4
+ language: Language | null;
5
+ loading: boolean;
6
+ error: Error | null;
7
+ refetch: () => void;
8
+ };
9
+ //# sourceMappingURL=useLanguage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLanguage.d.ts","sourceRoot":"","sources":["../src/useLanguage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAG1D,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,iBAAiB,GAC7B;IACD,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAiBA"}
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { useApiData } from './useApiData';
3
+ import {} from '@youversion/platform-core';
4
+ import { useLanguagesClient } from './useLanguageClient';
5
+ export function useLanguage(languageId, apiOptions) {
6
+ const languagesClient = useLanguagesClient();
7
+ const { data, loading, error, refetch } = useApiData(() => languagesClient.getLanguage(languageId), [languagesClient, languageId], {
8
+ enabled: apiOptions?.enabled !== false,
9
+ });
10
+ return {
11
+ language: data,
12
+ loading,
13
+ error,
14
+ refetch,
15
+ };
16
+ }
17
+ //# sourceMappingURL=useLanguage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLanguage.js","sourceRoot":"","sources":["../src/useLanguage.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAA0B,MAAM,cAAc,CAAC;AAClE,OAAO,EAAiB,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,UAAU,WAAW,CACzB,UAAkB,EAClB,UAA8B;IAO9B,MAAM,eAAe,GAAG,kBAAkB,EAAE,CAAC;IAE7C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,UAAU,CAClD,GAAG,EAAE,CAAC,eAAe,CAAC,WAAW,CAAC,UAAU,CAAC,EAC7C,CAAC,eAAe,EAAE,UAAU,CAAC,EAC7B;QACE,OAAO,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK;KACvC,CACF,CAAC;IAEF,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { LanguagesClient } from '@youversion/platform-core';
2
+ export declare function useLanguagesClient(): LanguagesClient;
3
+ //# sourceMappingURL=useLanguageClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLanguageClient.d.ts","sourceRoot":"","sources":["../src/useLanguageClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEvE,wBAAgB,kBAAkB,IAAI,eAAe,CAkBpD"}
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { useContext, useMemo } from 'react';
3
+ import { YouVersionContext } from './context';
4
+ import { ApiClient, LanguagesClient } from '@youversion/platform-core';
5
+ export function useLanguagesClient() {
6
+ const context = useContext(YouVersionContext);
7
+ return useMemo(() => {
8
+ if (!context?.appKey) {
9
+ throw new Error('YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.');
10
+ }
11
+ return new LanguagesClient(new ApiClient({
12
+ appKey: context.appKey,
13
+ apiHost: context.apiHost,
14
+ installationId: context.installationId,
15
+ }));
16
+ }, [context?.apiHost, context?.appKey, context?.installationId]);
17
+ }
18
+ //# sourceMappingURL=useLanguageClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLanguageClient.js","sourceRoot":"","sources":["../src/useLanguageClient.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAEvE,MAAM,UAAU,kBAAkB;IAChC,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAE9C,OAAO,OAAO,CAAC,GAAG,EAAE;QAClB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,uHAAuH,CACxH,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,eAAe,CACxB,IAAI,SAAS,CAAC;YACZ,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,cAAc,EAAE,OAAO,CAAC,cAAc;SACvC,CAAC,CACH,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;AACnE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useLanguages.d.ts","sourceRoot":"","sources":["../src/useLanguages.ts"],"names":[],"mappings":"AAMA,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,2BAA2B,CAAC;AAEnC,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,EACjC,UAAU,CAAC,EAAE,iBAAiB,GAC7B;IACD,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAgCA"}
1
+ {"version":3,"file":"useLanguages.d.ts","sourceRoot":"","sources":["../src/useLanguages.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,2BAA2B,CAAC;AAGnC,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,EACjC,UAAU,CAAC,EAAE,iBAAiB,GAC7B;IACD,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAiBA"}
@@ -1,22 +1,9 @@
1
1
  'use client';
2
- import { useMemo } from 'react';
3
- import { useContext } from 'react';
4
- import { YouVersionContext } from './context';
5
- import { LanguagesClient, ApiClient } from '@youversion/platform-core';
6
2
  import { useApiData } from './useApiData';
7
3
  import {} from '@youversion/platform-core';
4
+ import { useLanguagesClient } from './useLanguageClient';
8
5
  export function useLanguages(options = {}, apiOptions) {
9
- const context = useContext(YouVersionContext);
10
- const languagesClient = useMemo(() => {
11
- if (!context?.appKey) {
12
- throw new Error('YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.');
13
- }
14
- return new LanguagesClient(new ApiClient({
15
- appKey: context.appKey,
16
- apiHost: context.apiHost,
17
- installationId: context.installationId,
18
- }));
19
- }, [context?.apiHost, context?.appKey, context?.installationId]);
6
+ const languagesClient = useLanguagesClient();
20
7
  const { data, loading, error, refetch } = useApiData(() => languagesClient.getLanguages(options), [languagesClient, options?.country, options?.page_size, options?.page_token], {
21
8
  enabled: apiOptions?.enabled !== false,
22
9
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useLanguages.js","sourceRoot":"","sources":["../src/useLanguages.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,UAAU,EAA0B,MAAM,cAAc,CAAC;AAClE,OAAO,EAIN,MAAM,2BAA2B,CAAC;AAEnC,MAAM,UAAU,YAAY,CAC1B,UAA+B,EAAE,EACjC,UAA8B;IAO9B,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAE9C,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,EAAE;QACnC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CACb,uHAAuH,CACxH,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,eAAe,CACxB,IAAI,SAAS,CAAC;YACZ,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,cAAc,EAAE,OAAO,CAAC,cAAc;SACvC,CAAC,CACH,CAAC;IACJ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC;IAEjE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,UAAU,CAClD,GAAG,EAAE,CAAC,eAAe,CAAC,YAAY,CAAC,OAAO,CAAC,EAC3C,CAAC,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,EAC5E;QACE,OAAO,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK;KACvC,CACF,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,IAAI;QACf,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"useLanguages.js","sourceRoot":"","sources":["../src/useLanguages.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,UAAU,EAA0B,MAAM,cAAc,CAAC;AAClE,OAAO,EAIN,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,UAAU,YAAY,CAC1B,UAA+B,EAAE,EACjC,UAA8B;IAO9B,MAAM,eAAe,GAAG,kBAAkB,EAAE,CAAC;IAE7C,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,UAAU,CAClD,GAAG,EAAE,CAAC,eAAe,CAAC,YAAY,CAAC,OAAO,CAAC,EAC3C,CAAC,eAAe,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,EAC5E;QACE,OAAO,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK;KACvC,CACF,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,IAAI;QACf,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-react-hooks",
3
- "version": "1.8.0",
3
+ "version": "1.9.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.8.0"
25
+ "@youversion/platform-core": "1.9.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=19.1.0 <20.0.0"
@@ -37,8 +37,8 @@
37
37
  "jsdom": "27.0.1",
38
38
  "typescript": "5.9.3",
39
39
  "vitest": "4.0.4",
40
- "@internal/eslint-config": "0.0.0",
41
- "@internal/tsconfig": "0.0.0"
40
+ "@internal/tsconfig": "0.0.0",
41
+ "@internal/eslint-config": "0.0.0"
42
42
  },
43
43
  "scripts": {
44
44
  "dev": "tsc --watch",
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from './usePassage';
18
18
  export * from './useVOTD';
19
19
  export * from './useHighlights';
20
20
  export * from './useLanguages';
21
+ export * from './useLanguage';
21
22
  export * from './useTheme';
22
23
 
23
24
  // Auth hooks
@@ -0,0 +1,116 @@
1
+ import { renderHook, waitFor, act } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { useLanguage } from './useLanguage';
4
+ import { type LanguagesClient } from '@youversion/platform-core';
5
+ import { useLanguagesClient } from './useLanguageClient';
6
+
7
+ vi.mock('./useLanguageClient');
8
+
9
+ describe('useLanguage', () => {
10
+ const mockGetLanguage = vi.fn();
11
+
12
+ const mockLanguage = {
13
+ id: 'en',
14
+ language: 'en',
15
+ script: 'Latn',
16
+ script_name: 'Latin',
17
+ aliases: ['eng'],
18
+ display_names: { en: 'English' },
19
+ scripts: ['Latn'],
20
+ variants: [],
21
+ countries: ['US', 'GB', 'CA', 'AU'],
22
+ text_direction: 'ltr',
23
+ writing_population: 1500000000,
24
+ speaking_population: 1500000000,
25
+ default_bible_id: 111,
26
+ };
27
+
28
+ beforeEach(() => {
29
+ vi.resetAllMocks();
30
+
31
+ mockGetLanguage.mockResolvedValue(mockLanguage);
32
+
33
+ const mockClient: Partial<LanguagesClient> = { getLanguage: mockGetLanguage };
34
+ vi.mocked(useLanguagesClient).mockReturnValue(mockClient as LanguagesClient);
35
+ });
36
+
37
+ describe('fetching language', () => {
38
+ it('should fetch a language by id', async () => {
39
+ const { result } = renderHook(() => useLanguage('en'));
40
+
41
+ expect(result.current.loading).toBe(true);
42
+ expect(result.current.language).toBe(null);
43
+
44
+ await waitFor(() => {
45
+ expect(result.current.loading).toBe(false);
46
+ });
47
+
48
+ expect(mockGetLanguage).toHaveBeenCalledWith('en');
49
+ expect(result.current.language).toEqual(mockLanguage);
50
+ });
51
+
52
+ it('should refetch when languageId changes', async () => {
53
+ const { result, rerender } = renderHook(({ languageId }) => useLanguage(languageId), {
54
+ initialProps: { languageId: 'en' },
55
+ });
56
+
57
+ await waitFor(() => {
58
+ expect(result.current.loading).toBe(false);
59
+ });
60
+
61
+ expect(mockGetLanguage).toHaveBeenCalledTimes(1);
62
+ expect(mockGetLanguage).toHaveBeenCalledWith('en');
63
+
64
+ rerender({ languageId: 'es' });
65
+
66
+ await waitFor(() => {
67
+ expect(mockGetLanguage).toHaveBeenCalledTimes(2);
68
+ });
69
+
70
+ expect(mockGetLanguage).toHaveBeenLastCalledWith('es');
71
+ });
72
+
73
+ it('should not fetch when enabled is false', async () => {
74
+ const { result } = renderHook(() => useLanguage('en', { enabled: false }));
75
+
76
+ await waitFor(() => {
77
+ expect(result.current.loading).toBe(false);
78
+ });
79
+
80
+ expect(mockGetLanguage).not.toHaveBeenCalled();
81
+ expect(result.current.language).toBe(null);
82
+ });
83
+
84
+ it('should handle fetch errors', async () => {
85
+ const error = new Error('Failed to fetch language');
86
+ mockGetLanguage.mockRejectedValueOnce(error);
87
+
88
+ const { result } = renderHook(() => useLanguage('en'));
89
+
90
+ await waitFor(() => {
91
+ expect(result.current.loading).toBe(false);
92
+ });
93
+
94
+ expect(result.current.error).toEqual(error);
95
+ expect(result.current.language).toBe(null);
96
+ });
97
+
98
+ it('should support manual refetch', async () => {
99
+ const { result } = renderHook(() => useLanguage('en'));
100
+
101
+ await waitFor(() => {
102
+ expect(result.current.loading).toBe(false);
103
+ });
104
+
105
+ expect(mockGetLanguage).toHaveBeenCalledTimes(1);
106
+
107
+ act(() => {
108
+ result.current.refetch();
109
+ });
110
+
111
+ await waitFor(() => {
112
+ expect(mockGetLanguage).toHaveBeenCalledTimes(2);
113
+ });
114
+ });
115
+ });
116
+ });
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { useApiData, type UseApiDataOptions } from './useApiData';
4
+ import { type Language } from '@youversion/platform-core';
5
+ import { useLanguagesClient } from './useLanguageClient';
6
+
7
+ export function useLanguage(
8
+ languageId: string,
9
+ apiOptions?: UseApiDataOptions,
10
+ ): {
11
+ language: Language | null;
12
+ loading: boolean;
13
+ error: Error | null;
14
+ refetch: () => void;
15
+ } {
16
+ const languagesClient = useLanguagesClient();
17
+
18
+ const { data, loading, error, refetch } = useApiData<Language>(
19
+ () => languagesClient.getLanguage(languageId),
20
+ [languagesClient, languageId],
21
+ {
22
+ enabled: apiOptions?.enabled !== false,
23
+ },
24
+ );
25
+
26
+ return {
27
+ language: data,
28
+ loading,
29
+ error,
30
+ refetch,
31
+ };
32
+ }
@@ -0,0 +1,112 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import type { ReactNode } from 'react';
4
+ import { useLanguagesClient } from './useLanguageClient';
5
+ import { YouVersionContext } from './context';
6
+ import { LanguagesClient, ApiClient } from '@youversion/platform-core';
7
+
8
+ // Mock the core package
9
+ vi.mock('@youversion/platform-core', async () => {
10
+ const actual = await vi.importActual('@youversion/platform-core');
11
+ return {
12
+ ...actual,
13
+ LanguagesClient: vi.fn(function () {
14
+ return {};
15
+ }),
16
+ ApiClient: vi.fn(function () {
17
+ return { isApiClient: true };
18
+ }),
19
+ };
20
+ });
21
+
22
+ describe('useLanguagesClient', () => {
23
+ const mockAppKey = 'test-app-key';
24
+
25
+ const createWrapper = (contextValue: { appKey: string }) => {
26
+ return ({ children }: { children: ReactNode }) => (
27
+ <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
28
+ );
29
+ };
30
+
31
+ beforeEach(() => {
32
+ vi.resetAllMocks();
33
+
34
+ vi.mocked(LanguagesClient).mockImplementation(function () {
35
+ const mockClient: Partial<LanguagesClient> = { getLanguages: vi.fn() };
36
+ return mockClient;
37
+ });
38
+
39
+ vi.mocked(ApiClient).mockImplementation(function () {
40
+ const mockApiClient = { isApiClient: true };
41
+ return mockApiClient;
42
+ });
43
+ });
44
+
45
+ describe('context validation', () => {
46
+ it('should throw error when context is not provided', () => {
47
+ expect(() => renderHook(() => useLanguagesClient())).toThrow(
48
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
49
+ );
50
+ });
51
+
52
+ it('should throw error when appKey is missing', () => {
53
+ const wrapper = createWrapper({
54
+ appKey: '',
55
+ });
56
+
57
+ expect(() => renderHook(() => useLanguagesClient(), { wrapper })).toThrow(
58
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
59
+ );
60
+ });
61
+ });
62
+
63
+ describe('client creation', () => {
64
+ it('should create LanguagesClient with correct ApiClient config', () => {
65
+ const wrapper = createWrapper({
66
+ appKey: mockAppKey,
67
+ });
68
+
69
+ renderHook(() => useLanguagesClient(), { wrapper });
70
+
71
+ expect(ApiClient).toHaveBeenCalledWith({
72
+ appKey: mockAppKey,
73
+ });
74
+ expect(LanguagesClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
75
+ });
76
+
77
+ it('should memoize LanguagesClient instance', () => {
78
+ const wrapper = createWrapper({
79
+ appKey: mockAppKey,
80
+ });
81
+
82
+ const { rerender } = renderHook(() => useLanguagesClient(), { wrapper });
83
+
84
+ rerender();
85
+
86
+ expect(LanguagesClient).toHaveBeenCalledTimes(1);
87
+ });
88
+
89
+ it('should create new LanguagesClient when context values change', () => {
90
+ let currentAppKey = mockAppKey;
91
+
92
+ const wrapper = ({ children }: { children: ReactNode }) => (
93
+ <YouVersionContext.Provider
94
+ value={{
95
+ appKey: currentAppKey,
96
+ }}
97
+ >
98
+ {children}
99
+ </YouVersionContext.Provider>
100
+ );
101
+
102
+ const { rerender } = renderHook(() => useLanguagesClient(), { wrapper });
103
+
104
+ expect(LanguagesClient).toHaveBeenCalledTimes(1);
105
+
106
+ currentAppKey = 'new-app-key';
107
+ rerender();
108
+
109
+ expect(LanguagesClient).toHaveBeenCalledTimes(2);
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+
3
+ import { useContext, useMemo } from 'react';
4
+ import { YouVersionContext } from './context';
5
+ import { ApiClient, LanguagesClient } from '@youversion/platform-core';
6
+
7
+ export function useLanguagesClient(): LanguagesClient {
8
+ const context = useContext(YouVersionContext);
9
+
10
+ return useMemo(() => {
11
+ if (!context?.appKey) {
12
+ throw new Error(
13
+ 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
14
+ );
15
+ }
16
+
17
+ return new LanguagesClient(
18
+ new ApiClient({
19
+ appKey: context.appKey,
20
+ apiHost: context.apiHost,
21
+ installationId: context.installationId,
22
+ }),
23
+ );
24
+ }, [context?.apiHost, context?.appKey, context?.installationId]);
25
+ }
@@ -1,32 +1,21 @@
1
1
  import { renderHook, waitFor } from '@testing-library/react';
2
- import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
3
  import type { ReactNode } from 'react';
4
4
  import { useLanguages } from './useLanguages';
5
5
  import { YouVersionContext } from './context';
6
6
  import {
7
- LanguagesClient,
8
- ApiClient,
7
+ type LanguagesClient,
9
8
  type Collection,
10
9
  type Language,
11
10
  type GetLanguagesOptions,
12
11
  } from '@youversion/platform-core';
12
+ import { useLanguagesClient } from './useLanguageClient';
13
13
 
14
- // Mock the core package
15
- vi.mock('@youversion/platform-core', async () => {
16
- const actual = await vi.importActual('@youversion/platform-core');
17
- return {
18
- ...actual,
19
- LanguagesClient: vi.fn(function () {
20
- return {};
21
- }),
22
- ApiClient: vi.fn(function () {
23
- return { isApiClient: true };
24
- }),
25
- };
26
- });
14
+ vi.mock('./useLanguageClient');
27
15
 
28
16
  describe('useLanguages', () => {
29
17
  const mockAppKey = 'test-app-key';
18
+ const mockGetLanguages = vi.fn();
30
19
 
31
20
  const mockLanguages: Collection<Language> = {
32
21
  data: [
@@ -46,7 +35,7 @@ describe('useLanguages', () => {
46
35
  text_direction: 'ltr',
47
36
  writing_population: 370000000,
48
37
  speaking_population: 1500000000,
49
- default_bible_version_id: 1,
38
+ default_bible_id: 1,
50
39
  },
51
40
  {
52
41
  id: 'es',
@@ -64,14 +53,12 @@ describe('useLanguages', () => {
64
53
  text_direction: 'ltr',
65
54
  writing_population: 470000000,
66
55
  speaking_population: 580000000,
67
- default_bible_version_id: 128,
56
+ default_bible_id: 128,
68
57
  },
69
58
  ],
70
59
  next_page_token: null,
71
60
  };
72
61
 
73
- let mockGetLanguages: Mock;
74
-
75
62
  const createWrapper = (contextValue: { appKey: string }) => {
76
63
  return ({ children }: { children: ReactNode }) => (
77
64
  <YouVersionContext.Provider value={contextValue}>{children}</YouVersionContext.Provider>
@@ -79,91 +66,12 @@ describe('useLanguages', () => {
79
66
  };
80
67
 
81
68
  beforeEach(() => {
82
- vi.clearAllMocks();
69
+ vi.resetAllMocks();
83
70
 
84
- mockGetLanguages = vi.fn().mockResolvedValue(mockLanguages);
85
-
86
- (LanguagesClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
87
- return {
88
- getLanguages: mockGetLanguages,
89
- };
90
- });
71
+ mockGetLanguages.mockResolvedValue(mockLanguages);
91
72
 
92
- (ApiClient as unknown as ReturnType<typeof vi.fn>).mockImplementation(function () {
93
- return {
94
- isApiClient: true,
95
- };
96
- });
97
- });
98
-
99
- describe('context validation', () => {
100
- it('should throw error when context is not provided', () => {
101
- expect(() => renderHook(() => useLanguages({ country: 'US' }))).toThrow(
102
- 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
103
- );
104
- });
105
-
106
- it('should throw error when appKey is missing', () => {
107
- const wrapper = createWrapper({
108
- appKey: '',
109
- });
110
-
111
- expect(() => renderHook(() => useLanguages({ country: 'US' }), { wrapper })).toThrow(
112
- 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
113
- );
114
- });
115
- });
116
-
117
- describe('client creation', () => {
118
- it('should create LanguagesClient with correct ApiClient config', () => {
119
- const wrapper = createWrapper({
120
- appKey: mockAppKey,
121
- });
122
-
123
- renderHook(() => useLanguages({ country: 'US' }), { wrapper });
124
-
125
- expect(ApiClient).toHaveBeenCalledWith({
126
- appKey: mockAppKey,
127
- });
128
- expect(LanguagesClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
129
- });
130
-
131
- it('should memoize LanguagesClient instance', () => {
132
- const wrapper = createWrapper({
133
- appKey: mockAppKey,
134
- });
135
-
136
- const { result, rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
137
- const _firstRefetch = result.current.refetch;
138
-
139
- rerender();
140
- const _secondRefetch = result.current.refetch;
141
-
142
- expect(LanguagesClient).toHaveBeenCalledTimes(1);
143
- });
144
-
145
- it('should create new LanguagesClient when context values change', () => {
146
- let currentAppKey = mockAppKey;
147
-
148
- const wrapper = ({ children }: { children: ReactNode }) => (
149
- <YouVersionContext.Provider
150
- value={{
151
- appKey: currentAppKey,
152
- }}
153
- >
154
- {children}
155
- </YouVersionContext.Provider>
156
- );
157
-
158
- const { rerender } = renderHook(() => useLanguages({ country: 'US' }), { wrapper });
159
-
160
- expect(LanguagesClient).toHaveBeenCalledTimes(1);
161
-
162
- currentAppKey = 'new-app-key';
163
- rerender();
164
-
165
- expect(LanguagesClient).toHaveBeenCalledTimes(2);
166
- });
73
+ const mockClient: Partial<LanguagesClient> = { getLanguages: mockGetLanguages };
74
+ vi.mocked(useLanguagesClient).mockReturnValue(mockClient as LanguagesClient);
167
75
  });
168
76
 
169
77
  describe('fetching languages', () => {
@@ -1,15 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
- import { useContext } from 'react';
5
- import { YouVersionContext } from './context';
6
- import { LanguagesClient, ApiClient } from '@youversion/platform-core';
7
3
  import { useApiData, type UseApiDataOptions } from './useApiData';
8
4
  import {
9
5
  type GetLanguagesOptions,
10
6
  type Collection,
11
7
  type Language,
12
8
  } from '@youversion/platform-core';
9
+ import { useLanguagesClient } from './useLanguageClient';
13
10
 
14
11
  export function useLanguages(
15
12
  options: GetLanguagesOptions = {},
@@ -20,22 +17,7 @@ export function useLanguages(
20
17
  error: Error | null;
21
18
  refetch: () => void;
22
19
  } {
23
- const context = useContext(YouVersionContext);
24
-
25
- const languagesClient = useMemo(() => {
26
- if (!context?.appKey) {
27
- throw new Error(
28
- 'YouVersion context not found. Make sure your component is wrapped with YouVersionProvider and an API key is provided.',
29
- );
30
- }
31
- return new LanguagesClient(
32
- new ApiClient({
33
- appKey: context.appKey,
34
- apiHost: context.apiHost,
35
- installationId: context.installationId,
36
- }),
37
- );
38
- }, [context?.apiHost, context?.appKey, context?.installationId]);
20
+ const languagesClient = useLanguagesClient();
39
21
 
40
22
  const { data, loading, error, refetch } = useApiData<Collection<Language>>(
41
23
  () => languagesClient.getLanguages(options),
package/vitest.config.ts CHANGED
@@ -11,6 +11,8 @@ export default defineConfig({
11
11
  provider: 'v8',
12
12
  reporter: ['text', 'json-summary'],
13
13
  reportsDirectory: './coverage',
14
+ all: true,
15
+ include: ['src/**/*.{ts,tsx}'],
14
16
  },
15
17
  },
16
18
  });