@youversion/platform-core 0.4.1

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 (59) hide show
  1. package/.env.example +7 -0
  2. package/.env.local +10 -0
  3. package/.turbo/turbo-build.log +18 -0
  4. package/CHANGELOG.md +7 -0
  5. package/LICENSE +201 -0
  6. package/README.md +369 -0
  7. package/dist/index.cjs +1330 -0
  8. package/dist/index.d.cts +737 -0
  9. package/dist/index.d.ts +737 -0
  10. package/dist/index.js +1286 -0
  11. package/package.json +46 -0
  12. package/src/AuthenticationStrategy.ts +78 -0
  13. package/src/SignInWithYouVersionResult.ts +53 -0
  14. package/src/StorageStrategy.ts +81 -0
  15. package/src/URLBuilder.ts +71 -0
  16. package/src/Users.ts +137 -0
  17. package/src/WebAuthenticationStrategy.ts +127 -0
  18. package/src/YouVersionAPI.ts +27 -0
  19. package/src/YouVersionPlatformConfiguration.ts +80 -0
  20. package/src/YouVersionUserInfo.ts +49 -0
  21. package/src/__tests__/StorageStrategy.test.ts +404 -0
  22. package/src/__tests__/URLBuilder.test.ts +289 -0
  23. package/src/__tests__/YouVersionPlatformConfiguration.test.ts +150 -0
  24. package/src/__tests__/authentication.test.ts +174 -0
  25. package/src/__tests__/bible.test.ts +356 -0
  26. package/src/__tests__/client.test.ts +109 -0
  27. package/src/__tests__/handlers.ts +41 -0
  28. package/src/__tests__/highlights.test.ts +485 -0
  29. package/src/__tests__/languages.test.ts +139 -0
  30. package/src/__tests__/setup.ts +17 -0
  31. package/src/authentication.ts +27 -0
  32. package/src/bible.ts +272 -0
  33. package/src/client.ts +162 -0
  34. package/src/highlight.ts +16 -0
  35. package/src/highlights.ts +173 -0
  36. package/src/index.ts +20 -0
  37. package/src/languages.ts +80 -0
  38. package/src/schemas/bible-index.ts +48 -0
  39. package/src/schemas/book.ts +34 -0
  40. package/src/schemas/chapter.ts +24 -0
  41. package/src/schemas/collection.ts +28 -0
  42. package/src/schemas/highlight.ts +23 -0
  43. package/src/schemas/index.ts +11 -0
  44. package/src/schemas/language.ts +38 -0
  45. package/src/schemas/passage.ts +14 -0
  46. package/src/schemas/user.ts +10 -0
  47. package/src/schemas/verse.ts +17 -0
  48. package/src/schemas/version.ts +31 -0
  49. package/src/schemas/votd.ts +10 -0
  50. package/src/types/api-config.ts +9 -0
  51. package/src/types/auth.ts +15 -0
  52. package/src/types/book.ts +116 -0
  53. package/src/types/chapter.ts +5 -0
  54. package/src/types/highlight.ts +9 -0
  55. package/src/types/index.ts +22 -0
  56. package/src/utils/constants.ts +219 -0
  57. package/tsconfig.build.json +11 -0
  58. package/tsconfig.json +12 -0
  59. package/vitest.config.ts +9 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@youversion/platform-core",
3
+ "version": "0.4.1",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/youversion/platform-sdk-react",
11
+ "directory": "packages/core"
12
+ },
13
+ "main": "./dist/index.cjs",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "devDependencies": {
24
+ "@vitest/coverage-v8": "^4.0.4",
25
+ "dotenv-cli": "7.4.2",
26
+ "eslint": "9.38.0",
27
+ "jsdom": "^24.0.0",
28
+ "msw": "2.11.6",
29
+ "typescript": "5.9.3",
30
+ "vitest": "4.0.4",
31
+ "@internal/eslint-config": "0.0.0",
32
+ "@internal/tsconfig": "0.0.0"
33
+ },
34
+ "dependencies": {
35
+ "tsup": "8.5.0",
36
+ "zod": "4.1.12"
37
+ },
38
+ "scripts": {
39
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
40
+ "build": "tsup src/index.ts --format cjs,esm --dts",
41
+ "lint": "eslint . --max-warnings 0",
42
+ "test": "dotenv -e .env.local -- vitest run",
43
+ "test:watch": "dotenv -e .env.local -- vitest",
44
+ "test:coverage": "dotenv -e .env.local -- vitest run --coverage"
45
+ }
46
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Platform-agnostic authentication strategy interface
3
+ *
4
+ * Implementations should handle platform-specific authentication flows
5
+ * and return the callback URL containing the authentication result.
6
+ */
7
+ export interface AuthenticationStrategy {
8
+ /**
9
+ * Opens the authentication flow and returns the callback URL
10
+ *
11
+ * @param authUrl - The YouVersion authorization URL to open
12
+ * @returns Promise that resolves to the callback URL with auth result
13
+ * @throws Error if authentication fails or is cancelled
14
+ */
15
+ authenticate(authUrl: URL): Promise<URL>;
16
+ }
17
+
18
+ /**
19
+ * Registry for platform-specific authentication strategies
20
+ *
21
+ * This singleton registry manages the current authentication strategy
22
+ * and ensures only one strategy is active at a time.
23
+ */
24
+ export class AuthenticationStrategyRegistry {
25
+ private static strategy: AuthenticationStrategy | null = null;
26
+
27
+ /**
28
+ * Registers a platform-specific authentication strategy
29
+ *
30
+ * @param strategy - The authentication strategy to register
31
+ * @throws Error if strategy is null, undefined, or missing required methods
32
+ */
33
+ static register(strategy: AuthenticationStrategy): void {
34
+ if (!strategy) {
35
+ throw new Error('Authentication strategy cannot be null or undefined');
36
+ }
37
+
38
+ if (typeof strategy.authenticate !== 'function') {
39
+ throw new Error('Authentication strategy must implement authenticate method');
40
+ }
41
+
42
+ this.strategy = strategy;
43
+ }
44
+
45
+ /**
46
+ * Gets the currently registered authentication strategy
47
+ *
48
+ * @returns The registered authentication strategy
49
+ * @throws Error if no strategy has been registered
50
+ */
51
+ static get(): AuthenticationStrategy {
52
+ if (!this.strategy) {
53
+ throw new Error(
54
+ 'No authentication strategy registered. Please register a platform-specific strategy using AuthenticationStrategyRegistry.register()',
55
+ );
56
+ }
57
+ return this.strategy;
58
+ }
59
+
60
+ /**
61
+ * Checks if a strategy is currently registered
62
+ *
63
+ * @returns true if a strategy is registered, false otherwise
64
+ */
65
+ static isRegistered(): boolean {
66
+ return this.strategy !== null;
67
+ }
68
+
69
+ /**
70
+ * Resets the registry by removing the current strategy
71
+ *
72
+ * This method is primarily intended for testing scenarios
73
+ * where you need to clean up between test cases.
74
+ */
75
+ static reset(): void {
76
+ this.strategy = null;
77
+ }
78
+ }
@@ -0,0 +1,53 @@
1
+ import type { SignInWithYouVersionPermissionValues } from './types';
2
+
3
+ export const SignInWithYouVersionPermission = {
4
+ bibles: 'bibles',
5
+ highlights: 'highlights',
6
+ votd: 'votd',
7
+ demographics: 'demographics',
8
+ bibleActivity: 'bible_activity',
9
+ } as const;
10
+
11
+ export class SignInWithYouVersionResult {
12
+ public readonly accessToken: string | null;
13
+ public readonly permissions: SignInWithYouVersionPermissionValues[];
14
+ public readonly errorMsg: string | null;
15
+ public readonly yvpUserId: string | null;
16
+
17
+ constructor(url: URL) {
18
+ const queryParams = new URLSearchParams(url.search);
19
+
20
+ const status = queryParams.get('status');
21
+ const userId = queryParams.get('yvp_user_id');
22
+ const latValue = queryParams.get('lat');
23
+ const grants = queryParams.get('grants');
24
+
25
+ const perms =
26
+ grants
27
+ ?.split(',')
28
+ .map((grant) => grant.trim())
29
+ .filter((grant) =>
30
+ Object.values(SignInWithYouVersionPermission).includes(
31
+ grant as SignInWithYouVersionPermissionValues,
32
+ ),
33
+ )
34
+ .map((grant) => grant as SignInWithYouVersionPermissionValues) ?? [];
35
+
36
+ if (status === 'success' && latValue && userId) {
37
+ this.accessToken = latValue;
38
+ this.permissions = perms;
39
+ this.errorMsg = null;
40
+ this.yvpUserId = userId;
41
+ } else if (status === 'canceled') {
42
+ this.accessToken = null;
43
+ this.permissions = [];
44
+ this.errorMsg = null;
45
+ this.yvpUserId = null;
46
+ } else {
47
+ this.accessToken = null;
48
+ this.permissions = [];
49
+ this.errorMsg = 'Authentication failed';
50
+ this.yvpUserId = null;
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Abstract storage strategy for auth-related data (callbacks, return URLs).
3
+ * Implementations can use different mechanisms (sessionStorage, memory, etc.)
4
+ *
5
+ * WARNING: Session storage has known XSS vulnerabilities. An attacker that gains
6
+ * script execution access (via XSS) can read all values in sessionStorage. Consider
7
+ * using a secure, HTTP-only cookie mechanism for production applications.
8
+ */
9
+ export interface StorageStrategy {
10
+ setItem(key: string, value: string): void;
11
+ getItem(key: string): string | null;
12
+ removeItem(key: string): void;
13
+ clear(): void;
14
+ }
15
+
16
+ /**
17
+ * Default storage strategy using the browser's sessionStorage.
18
+ * This provides temporary storage for authentication state during a single session.
19
+ *
20
+ * SECURITY WARNING: SessionStorage is vulnerable to XSS attacks. If an attacker
21
+ * can inject JavaScript into your application, they can access all sessionStorage values.
22
+ * For production applications handling sensitive authentication tokens, consider:
23
+ * 1. Using secure, HTTP-only cookies instead
24
+ * 2. Storing tokens in memory only (with redirection to re-authenticate on page reload)
25
+ * 3. Using a custom storage backend that implements additional security measures
26
+ */
27
+ export class SessionStorageStrategy implements StorageStrategy {
28
+ setItem(key: string, value: string): void {
29
+ if (typeof sessionStorage === 'undefined') {
30
+ console.warn('SessionStorage is not available in this environment');
31
+ return;
32
+ }
33
+ sessionStorage.setItem(key, value);
34
+ }
35
+
36
+ getItem(key: string): string | null {
37
+ if (typeof sessionStorage === 'undefined') {
38
+ return null;
39
+ }
40
+ return sessionStorage.getItem(key);
41
+ }
42
+
43
+ removeItem(key: string): void {
44
+ if (typeof sessionStorage === 'undefined') {
45
+ return;
46
+ }
47
+ sessionStorage.removeItem(key);
48
+ }
49
+
50
+ clear(): void {
51
+ if (typeof sessionStorage === 'undefined') {
52
+ return;
53
+ }
54
+ sessionStorage.clear();
55
+ }
56
+ }
57
+
58
+ /**
59
+ * In-memory storage strategy that stores data in a Map.
60
+ * This storage is cleared when the page is refreshed.
61
+ * Provides better XSS protection than sessionStorage but requires re-authentication on page reload.
62
+ */
63
+ export class MemoryStorageStrategy implements StorageStrategy {
64
+ private store = new Map<string, string>();
65
+
66
+ setItem(key: string, value: string): void {
67
+ this.store.set(key, value);
68
+ }
69
+
70
+ getItem(key: string): string | null {
71
+ return this.store.get(key) ?? null;
72
+ }
73
+
74
+ removeItem(key: string): void {
75
+ this.store.delete(key);
76
+ }
77
+
78
+ clear(): void {
79
+ this.store.clear();
80
+ }
81
+ }
@@ -0,0 +1,71 @@
1
+ import type { SignInWithYouVersionPermissionValues } from './types';
2
+ import { YouVersionPlatformConfiguration } from './YouVersionPlatformConfiguration';
3
+
4
+ export class URLBuilder {
5
+ private static get baseURL(): URL {
6
+ return new URL(`https://${YouVersionPlatformConfiguration.apiHost}`);
7
+ }
8
+
9
+ static authURL(
10
+ appId: string,
11
+ requiredPermissions: Set<SignInWithYouVersionPermissionValues> = new Set<SignInWithYouVersionPermissionValues>(),
12
+ optionalPermissions: Set<SignInWithYouVersionPermissionValues> = new Set<SignInWithYouVersionPermissionValues>(),
13
+ ): URL {
14
+ if (typeof appId !== 'string' || appId.trim().length === 0) {
15
+ throw new Error('appId must be a non-empty string');
16
+ }
17
+
18
+ try {
19
+ const url = new URL(this.baseURL);
20
+ url.pathname = '/auth/login';
21
+
22
+ // Add query parameters
23
+ const searchParams = new URLSearchParams();
24
+ searchParams.append('app_id', appId);
25
+ searchParams.append('language', 'en'); // TODO: load from system
26
+
27
+ if (requiredPermissions.size > 0) {
28
+ const requiredList = Array.from(requiredPermissions).map((p) => p.toString());
29
+ searchParams.append('required_perms', requiredList.join(','));
30
+ }
31
+
32
+ if (optionalPermissions.size > 0) {
33
+ const optionalList = Array.from(optionalPermissions).map((p) => p.toString());
34
+ searchParams.append('opt_perms', optionalList.join(','));
35
+ }
36
+
37
+ const installationId = YouVersionPlatformConfiguration.installationId;
38
+ if (installationId) {
39
+ searchParams.append('x-yvp-installation-id', installationId);
40
+ }
41
+
42
+ url.search = searchParams.toString();
43
+ return url;
44
+ } catch (error) {
45
+ throw new Error(
46
+ `Failed to construct auth URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
47
+ );
48
+ }
49
+ }
50
+
51
+ static userURL(accessToken: string): URL {
52
+ if (typeof accessToken !== 'string' || accessToken.trim().length === 0) {
53
+ throw new Error('accessToken must be a non-empty string');
54
+ }
55
+
56
+ try {
57
+ const url = new URL(this.baseURL);
58
+ url.pathname = '/auth/me';
59
+
60
+ const searchParams = new URLSearchParams();
61
+ searchParams.append('lat', accessToken);
62
+
63
+ url.search = searchParams.toString();
64
+ return url;
65
+ } catch (error) {
66
+ throw new Error(
67
+ `Failed to construct user URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
68
+ );
69
+ }
70
+ }
71
+ }
package/src/Users.ts ADDED
@@ -0,0 +1,137 @@
1
+ import type { SignInWithYouVersionPermissionValues } from './types';
2
+ import { SignInWithYouVersionResult } from './SignInWithYouVersionResult';
3
+ import { YouVersionUserInfo } from './YouVersionUserInfo';
4
+ import { YouVersionPlatformConfiguration } from './YouVersionPlatformConfiguration';
5
+ import { YouVersionAPI } from './YouVersionAPI';
6
+ import { URLBuilder } from './URLBuilder';
7
+ import { AuthenticationStrategyRegistry } from './AuthenticationStrategy';
8
+
9
+ const MAX_RETRY_ATTEMPTS = 3;
10
+ const RETRY_DELAY_MS = 1000;
11
+
12
+ export class YouVersionAPIUsers {
13
+ /**
14
+ * Presents the YouVersion login flow to the user and returns the login result upon completion.
15
+ *
16
+ * This function authenticates the user with YouVersion, requesting the specified required and optional permissions.
17
+ * The function returns a promise that resolves when the user completes or cancels the login flow,
18
+ * returning the login result containing the authorization code and granted permissions.
19
+ *
20
+ * @param requiredPermissions - The set of permissions that must be granted by the user for successful login.
21
+ * @param optionalPermissions - The set of permissions that will be requested from the user but are not required for successful login.
22
+ * @returns A Promise resolving to a SignInWithYouVersionResult containing the authorization code and granted permissions upon successful login.
23
+ * @throws An error if authentication fails or is cancelled by the user.
24
+ */
25
+ static async signIn(
26
+ requiredPermissions: Set<SignInWithYouVersionPermissionValues>,
27
+ optionalPermissions: Set<SignInWithYouVersionPermissionValues>,
28
+ ): Promise<SignInWithYouVersionResult> {
29
+ // Validate inputs
30
+ if (!requiredPermissions || !(requiredPermissions instanceof Set)) {
31
+ throw new Error('Invalid requiredPermissions: must be a Set');
32
+ }
33
+ if (!optionalPermissions || !(optionalPermissions instanceof Set)) {
34
+ throw new Error('Invalid optionalPermissions: must be a Set');
35
+ }
36
+
37
+ const appId = YouVersionPlatformConfiguration.appId;
38
+ if (!appId) {
39
+ throw new Error('YouVersionPlatformConfiguration.appId must be set before calling signIn');
40
+ }
41
+
42
+ const url = URLBuilder.authURL(appId, requiredPermissions, optionalPermissions);
43
+
44
+ // Use the registered authentication strategy
45
+ const strategy = AuthenticationStrategyRegistry.get();
46
+ const callbackUrl = await strategy.authenticate(url);
47
+ const result = new SignInWithYouVersionResult(callbackUrl);
48
+
49
+ if (result.accessToken) {
50
+ YouVersionPlatformConfiguration.setAccessToken(result.accessToken);
51
+ }
52
+
53
+ return result;
54
+ }
55
+
56
+ static signOut(): void {
57
+ YouVersionPlatformConfiguration.setAccessToken(null);
58
+ }
59
+
60
+ /**
61
+ * Retrieves user information for the authenticated user using the provided access token.
62
+ *
63
+ * This function fetches the user's profile information from the YouVersion API, decoding it into a YouVersionUserInfo model.
64
+ *
65
+ * @param accessToken - The access token obtained from the login process.
66
+ * @returns A Promise resolving to a YouVersionUserInfo object containing the user's profile information.
67
+ * @throws An error if the URL is invalid, the network request fails, or the response cannot be decoded.
68
+ */
69
+ static async userInfo(accessToken: string): Promise<YouVersionUserInfo> {
70
+ // Validate access token
71
+ if (!accessToken || typeof accessToken !== 'string') {
72
+ throw new Error('Invalid access token: must be a non-empty string');
73
+ }
74
+
75
+ // Check for preview mode if configured
76
+ if (YouVersionPlatformConfiguration.isPreviewMode && accessToken === 'preview') {
77
+ return (
78
+ YouVersionPlatformConfiguration.previewUserInfo ||
79
+ new YouVersionUserInfo({
80
+ first_name: 'Preview',
81
+ last_name: 'User',
82
+ id: 'preview-user',
83
+ avatar_url: undefined,
84
+ })
85
+ );
86
+ }
87
+
88
+ const url = URLBuilder.userURL(accessToken);
89
+
90
+ const request = YouVersionAPI.addStandardHeaders(url);
91
+
92
+ // Retry logic for transient failures
93
+ let lastError: Error | null = null;
94
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
95
+ try {
96
+ const response = await fetch(request);
97
+
98
+ if (response.status === 401) {
99
+ throw new Error(
100
+ 'Authentication failed: Invalid or expired access token. Please sign in again.',
101
+ );
102
+ }
103
+
104
+ if (response.status === 403) {
105
+ throw new Error('Access denied: Insufficient permissions to retrieve user information');
106
+ }
107
+
108
+ if (response.status !== 200) {
109
+ throw new Error(
110
+ `Failed to retrieve user information: Server responded with status ${response.status}`,
111
+ );
112
+ }
113
+
114
+ const data = (await response.json()) as YouVersionUserInfo;
115
+ return data;
116
+ } catch (error) {
117
+ lastError = error instanceof Error ? error : new Error('Failed to parse server response');
118
+
119
+ // Don't retry on authentication errors
120
+ if (
121
+ error instanceof Error &&
122
+ (error.message.includes('401') || error.message.includes('403'))
123
+ ) {
124
+ throw error;
125
+ }
126
+
127
+ // Retry on network errors
128
+ if (attempt < MAX_RETRY_ATTEMPTS) {
129
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * attempt));
130
+ continue;
131
+ }
132
+ }
133
+ }
134
+
135
+ throw lastError || new Error('Failed to retrieve user information after multiple attempts');
136
+ }
137
+ }
@@ -0,0 +1,127 @@
1
+ import type { AuthenticationStrategy } from './AuthenticationStrategy';
2
+ import { SessionStorageStrategy, type StorageStrategy } from './StorageStrategy';
3
+
4
+ /* Web-based authentication strategy
5
+ * Uses redirect flow
6
+ */
7
+ export class WebAuthenticationStrategy implements AuthenticationStrategy {
8
+ private redirectUri: string;
9
+ private callbackPath: string;
10
+ private timeout: number;
11
+ private storage: StorageStrategy;
12
+ private static pendingAuthResolve: ((url: URL) => void) | null = null;
13
+ private static pendingAuthReject: ((error: Error) => void) | null = null;
14
+ private static timeoutId: ReturnType<typeof setTimeout> | null = null;
15
+
16
+ constructor(options?: {
17
+ redirectUri?: string;
18
+ callbackPath?: string;
19
+ timeout?: number;
20
+ storage?: StorageStrategy;
21
+ }) {
22
+ this.callbackPath = options?.callbackPath ?? '/auth/callback';
23
+ this.redirectUri = options?.redirectUri ?? window.location.origin + this.callbackPath;
24
+ this.timeout = options?.timeout ?? 300000; // 5 minutes default
25
+ this.storage = options?.storage ?? new SessionStorageStrategy();
26
+ }
27
+
28
+ async authenticate(authUrl: URL): Promise<URL> {
29
+ // Update the redirect URI in the auth URL
30
+ authUrl.searchParams.set('redirect_uri', this.redirectUri);
31
+
32
+ return this.authenticateWithRedirect(authUrl);
33
+ }
34
+
35
+ /**
36
+ * Call this method when your app loads to handle the redirect callback
37
+ */
38
+ static handleCallback(callbackPath: string = '/auth/callback'): boolean {
39
+ const currentUrl = new URL(window.location.href);
40
+
41
+ // Check if this is a callback URL
42
+ if (currentUrl.pathname === callbackPath && currentUrl.searchParams.has('status')) {
43
+ // For web apps, use the HTTP URL directly (no need to convert to youversionauth scheme)
44
+ const callbackUrl = new URL(currentUrl.toString());
45
+
46
+ if (WebAuthenticationStrategy.pendingAuthResolve) {
47
+ WebAuthenticationStrategy.pendingAuthResolve(callbackUrl);
48
+ WebAuthenticationStrategy.cleanup();
49
+ } else {
50
+ // Store the callback URL for later retrieval
51
+ const storageStrategy = new SessionStorageStrategy();
52
+ storageStrategy.setItem('youversion-auth-callback', callbackUrl.toString());
53
+ }
54
+
55
+ const storageStrategy = new SessionStorageStrategy();
56
+ const returnUrl = storageStrategy.getItem('youversion-auth-return') ?? '/';
57
+ storageStrategy.removeItem('youversion-auth-return');
58
+ window.history.replaceState({}, '', returnUrl);
59
+
60
+ return true;
61
+ }
62
+
63
+ return false;
64
+ }
65
+
66
+ /**
67
+ * Clean up pending authentication state
68
+ */
69
+ static cleanup(): void {
70
+ if (WebAuthenticationStrategy.timeoutId) {
71
+ clearTimeout(WebAuthenticationStrategy.timeoutId);
72
+ WebAuthenticationStrategy.timeoutId = null;
73
+ }
74
+ WebAuthenticationStrategy.pendingAuthResolve = null;
75
+ WebAuthenticationStrategy.pendingAuthReject = null;
76
+ }
77
+
78
+ /**
79
+ * Retrieve stored callback result if available
80
+ */
81
+ static getStoredCallback(): URL | null {
82
+ const storageStrategy = new SessionStorageStrategy();
83
+ const stored = storageStrategy.getItem('youversion-auth-callback');
84
+ if (stored) {
85
+ storageStrategy.removeItem('youversion-auth-callback');
86
+ try {
87
+ return new URL(stored);
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ private authenticateWithRedirect(authUrl: URL): Promise<URL> {
96
+ // Clean up any existing state
97
+ WebAuthenticationStrategy.cleanup();
98
+
99
+ // Store return URL in configured storage
100
+ this.storage.setItem('youversion-auth-return', window.location.href);
101
+
102
+ // Set up the promise that will be resolved when we come back
103
+ return new Promise((resolve, reject) => {
104
+ WebAuthenticationStrategy.pendingAuthResolve = resolve;
105
+ WebAuthenticationStrategy.pendingAuthReject = reject;
106
+
107
+ // Set up timeout
108
+ WebAuthenticationStrategy.timeoutId = setTimeout(() => {
109
+ WebAuthenticationStrategy.cleanup();
110
+ reject(new Error('Authentication timeout'));
111
+ }, this.timeout);
112
+
113
+ // Handle cases where navigation might fail
114
+ try {
115
+ // Redirect to auth URL
116
+ window.location.href = authUrl.toString();
117
+ } catch (error) {
118
+ WebAuthenticationStrategy.cleanup();
119
+ reject(
120
+ new Error(
121
+ `Failed to navigate to auth URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
122
+ ),
123
+ );
124
+ }
125
+ });
126
+ }
127
+ }
@@ -0,0 +1,27 @@
1
+ import { YouVersionPlatformConfiguration } from './YouVersionPlatformConfiguration';
2
+
3
+ export class YouVersionAPI {
4
+ static addStandardHeaders(url: URL): Request {
5
+ const headers: Record<string, string> = {
6
+ Accept: 'application/json',
7
+ 'Content-Type': 'application/json',
8
+ };
9
+
10
+ // Add app ID header
11
+ const appId = YouVersionPlatformConfiguration.appId;
12
+ if (appId) {
13
+ headers['X-App-Id'] = appId;
14
+ }
15
+
16
+ // Add installation ID header
17
+ const installationId = YouVersionPlatformConfiguration.installationId;
18
+ if (installationId) {
19
+ headers['x-yvp-installation-id'] = installationId;
20
+ }
21
+
22
+ const request = new Request(url.toString(), {
23
+ headers,
24
+ });
25
+ return request;
26
+ }
27
+ }