@youversion/platform-core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Users.ts CHANGED
@@ -1,137 +1,418 @@
1
1
  import type { SignInWithYouVersionPermissionValues } from './types';
2
- import { SignInWithYouVersionResult } from './SignInWithYouVersionResult';
3
2
  import { YouVersionUserInfo } from './YouVersionUserInfo';
4
3
  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;
4
+ import { SignInWithYouVersionPKCEAuthorizationRequestBuilder } from './SignInWithYouVersionPKCE';
5
+ import { SignInWithYouVersionResult } from './SignInWithYouVersionResult';
6
+ import { SignInWithYouVersionPermission } from './SignInWithYouVersionResult';
11
7
 
12
8
  export class YouVersionAPIUsers {
13
9
  /**
14
10
  * Presents the YouVersion login flow to the user and returns the login result upon completion.
15
11
  *
16
12
  * 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.
13
+ * The function redirects to the YouVersion authorization URL and expects the callback to be handled separately.
19
14
  *
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.
15
+ * @param permissions - The set of permissions that must be granted by the user for successful login.
16
+ * @param redirectURL - The URL to redirect back to after authentication.
17
+ * @throws An error if authentication fails or configuration is invalid.
24
18
  */
25
19
  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
-
20
+ permissions: Set<SignInWithYouVersionPermissionValues>,
21
+ redirectURL: string,
22
+ ): Promise<void> {
37
23
  const appKey = YouVersionPlatformConfiguration.appKey;
38
24
  if (!appKey) {
39
25
  throw new Error('YouVersionPlatformConfiguration.appKey must be set before calling signIn');
40
26
  }
41
27
 
42
- const url = URLBuilder.authURL(appKey, requiredPermissions, optionalPermissions);
28
+ const authorizationRequest = await SignInWithYouVersionPKCEAuthorizationRequestBuilder.make(
29
+ appKey,
30
+ permissions,
31
+ new URL(redirectURL),
32
+ );
33
+
34
+ // Store auth data for callback handler
35
+ localStorage.setItem(
36
+ 'youversion-auth-code-verifier',
37
+ authorizationRequest.parameters.codeVerifier,
38
+ );
39
+ const redirectUrlString = redirectURL.toString().endsWith('/')
40
+ ? redirectURL.toString().slice(0, -1)
41
+ : redirectURL.toString();
42
+ localStorage.setItem('youversion-auth-redirect-uri', redirectUrlString);
43
+ localStorage.setItem('youversion-auth-state', authorizationRequest.parameters.state);
44
+
45
+ // Simple redirect to authorization URL
46
+ window.location.href = authorizationRequest.url.toString();
47
+ }
48
+
49
+ /**
50
+ * Handles the OAuth callback after user authentication.
51
+ *
52
+ * Call this method when your app loads to check if the current URL contains
53
+ * an OAuth callback with authorization code. If found, it exchanges the code
54
+ * for tokens and stores them.
55
+ *
56
+ * @returns Promise<SignInWithYouVersionResult | null> - SignInWithYouVersionResult if callback was handled, null otherwise
57
+ * @throws An error if token exchange fails
58
+ */
59
+ static async handleAuthCallback(): Promise<SignInWithYouVersionResult | null> {
60
+ const urlParams = new URLSearchParams(window.location.search);
61
+ const code = urlParams.get('code');
62
+ const state = urlParams.get('state');
63
+ const error = urlParams.get('error');
64
+
65
+ // Check if this is an OAuth callback
66
+ if (!state && !error) {
67
+ return null;
68
+ }
69
+
70
+ // Handle OAuth error
71
+ if (error) {
72
+ const errorDescription = urlParams.get('error_description') || error;
73
+ throw new Error(`OAuth authentication failed: ${errorDescription}`);
74
+ }
75
+
76
+ // Verify state parameter
77
+ const storedState = localStorage.getItem('youversion-auth-state');
78
+ if (state !== storedState) {
79
+ throw new Error('Invalid state parameter - possible CSRF attack');
80
+ }
81
+
82
+ // If we don't have a code, this might be the first callback with user data
83
+ // We need to redirect to the server callback to get the authorization code
84
+ if (!code && state) {
85
+ this.obtainLocation(window.location.href, state);
86
+ }
87
+
88
+ // Get stored auth data
89
+ const codeVerifier = localStorage.getItem('youversion-auth-code-verifier');
90
+ const redirectUri = localStorage.getItem('youversion-auth-redirect-uri');
91
+
92
+ if (!code || !codeVerifier || !redirectUri) {
93
+ throw new Error('Missing required authentication parameters');
94
+ }
95
+
96
+ try {
97
+ // Exchange authorization code for tokens
98
+ const tokenRequest = SignInWithYouVersionPKCEAuthorizationRequestBuilder.tokenURLRequest(
99
+ code,
100
+ codeVerifier,
101
+ redirectUri,
102
+ );
103
+
104
+ const response = await fetch(tokenRequest);
105
+
106
+ if (!response.ok) {
107
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
108
+ }
109
+
110
+ const responseText = await response.text();
111
+
112
+ const tokens = JSON.parse(responseText) as {
113
+ access_token: string;
114
+ expires_in: number;
115
+ id_token: string;
116
+ refresh_token: string;
117
+ scope: string;
118
+ token_type: string;
119
+ };
120
+
121
+ // Extract user info from ID token
122
+ const result = this.extractSignInResult(tokens);
123
+
124
+ // Store tokens in configuration
125
+ YouVersionPlatformConfiguration.saveAuthData(
126
+ result.accessToken || null,
127
+ result.refreshToken || null,
128
+ result.idToken || null,
129
+ result.expiryDate || null,
130
+ );
131
+
132
+ // Clean up localStorage
133
+ localStorage.removeItem('youversion-auth-code-verifier');
134
+ localStorage.removeItem('youversion-auth-redirect-uri');
135
+ localStorage.removeItem('youversion-auth-state');
136
+
137
+ // Clean up URL
138
+ const cleanUrl = new URL(window.location.href);
139
+ cleanUrl.search = '';
140
+ window.history.replaceState({}, '', cleanUrl.toString());
141
+
142
+ return result;
143
+ } catch (error) {
144
+ // Clean up on error
145
+ localStorage.removeItem('youversion-auth-code-verifier');
146
+ localStorage.removeItem('youversion-auth-redirect-uri');
147
+ localStorage.removeItem('youversion-auth-state');
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Redirects to the server callback endpoint to obtain authorization code
154
+ */
155
+ private static obtainLocation(callbackURL: string, state: string): void {
156
+ const url = new URL(callbackURL);
157
+ const params = new URLSearchParams(url.search);
158
+
159
+ if (params.get('state') !== state) {
160
+ throw new Error('Invalid state parameter');
161
+ }
162
+
163
+ // Redirect to the server callback endpoint with all the current parameters
164
+ const serverCallbackUrl = new URL(
165
+ `https://${YouVersionPlatformConfiguration.apiHost}/auth/callback`,
166
+ );
167
+ params.forEach((value, key) => {
168
+ serverCallbackUrl.searchParams.set(key, value);
169
+ });
170
+
171
+ window.location.href = serverCallbackUrl.toString();
172
+ }
173
+
174
+ /**
175
+ * Extracts sign-in result from token response
176
+ */
177
+ private static extractSignInResult(tokens: {
178
+ access_token: string;
179
+ expires_in: number;
180
+ id_token: string;
181
+ refresh_token: string;
182
+ scope: string;
183
+ token_type: string;
184
+ }): SignInWithYouVersionResult {
185
+ const idClaims = this.decodeJWT(tokens.id_token);
186
+
187
+ const permissions = tokens.scope
188
+ .split(' ')
189
+ .map((p) => p.trim())
190
+ .filter((p) => p.length > 0)
191
+ .filter((p): p is SignInWithYouVersionPermissionValues =>
192
+ Object.values(SignInWithYouVersionPermission).includes(
193
+ p as SignInWithYouVersionPermissionValues,
194
+ ),
195
+ );
196
+
197
+ const resultData = {
198
+ accessToken: tokens.access_token,
199
+ expiresIn: tokens.expires_in,
200
+ refreshToken: tokens.refresh_token,
201
+ idToken: tokens.id_token,
202
+ permissions,
203
+ yvpUserId: idClaims.sub as string,
204
+ name: idClaims.name as string,
205
+ profilePicture: idClaims.profile_picture as string,
206
+ email: idClaims.email as string,
207
+ };
43
208
 
44
- // Use the registered authentication strategy
45
- const strategy = AuthenticationStrategyRegistry.get();
46
- const callbackUrl = await strategy.authenticate(url);
47
- const result = new SignInWithYouVersionResult(callbackUrl);
209
+ return new SignInWithYouVersionResult(resultData);
210
+ }
211
+
212
+ /**
213
+ * Decodes JWT payload for UI display purposes.
214
+ *
215
+ * Note: We intentionally do not verify the JWT signature here because:
216
+ *
217
+ * 1. YouVersion's backend verifies all tokens on API requests
218
+ * 2. This decoded data is only used for UI display
219
+ * 3. No security decisions are made based on these claims
220
+ *
221
+ * @private
222
+ */
48
223
 
49
- if (result.accessToken) {
50
- YouVersionPlatformConfiguration.setAccessToken(result.accessToken);
224
+ private static decodeJWT(token: string): Record<string, any> {
225
+ const segments = token.split('.');
226
+
227
+ if (segments.length !== 3) {
228
+ return {};
229
+ }
230
+
231
+ let base64 = segments[1]?.replace(/-/g, '+').replace(/_/g, '/');
232
+
233
+ while (base64 && base64.length % 4 !== 0) {
234
+ base64 += '=';
51
235
  }
52
236
 
53
- return result;
237
+ try {
238
+ if (base64) {
239
+ const data = atob(base64);
240
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
241
+ return JSON.parse(data);
242
+ } else {
243
+ return {};
244
+ }
245
+ } catch (error) {
246
+ if (process.env.NODE_ENV === 'development') {
247
+ console.error('JWT decode failed:', error);
248
+ }
249
+ return {};
250
+ }
54
251
  }
55
252
 
56
253
  static signOut(): void {
57
- YouVersionPlatformConfiguration.setAccessToken(null);
254
+ YouVersionPlatformConfiguration.clearAuthTokens();
58
255
  }
59
256
 
60
257
  /**
61
- * Retrieves user information for the authenticated user using the provided access token.
258
+ * Retrieves user information for the authenticated user by decoding the provided JWT access token.
62
259
  *
63
- * This function fetches the user's profile information from the YouVersion API, decoding it into a YouVersionUserInfo model.
260
+ * This function extracts the user's profile information directly from the JWT token payload.
64
261
  *
65
- * @param accessToken - The access token obtained from the login process.
262
+ * @param accessToken - The JWT access token obtained from the login process.
66
263
  * @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.
264
+ * @throws An error if the access token is invalid or cannot be decoded.
68
265
  */
69
- static async userInfo(accessToken: string): Promise<YouVersionUserInfo> {
266
+ static userInfo(idToken: string): YouVersionUserInfo {
70
267
  // Validate access token
71
- if (!accessToken || typeof accessToken !== 'string') {
268
+ if (!idToken || typeof idToken !== 'string') {
72
269
  throw new Error('Invalid access token: must be a non-empty string');
73
270
  }
74
271
 
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
- })
272
+ try {
273
+ // Decode JWT payload to extract user information
274
+ const claims = this.decodeJWT(idToken);
275
+
276
+ if (!claims || Object.keys(claims).length === 0) {
277
+ throw new Error('Invalid JWT token: Unable to decode token payload');
278
+ }
279
+
280
+ // Map JWT claims to YouVersionUserInfo format
281
+ const userInfoData = {
282
+ id: claims.sub as string,
283
+ name: claims.name as string,
284
+ avatar_url: claims.profile_picture as string,
285
+ email: claims.email as string,
286
+ };
287
+
288
+ return new YouVersionUserInfo(userInfoData);
289
+ } catch (error) {
290
+ if (error instanceof Error) {
291
+ throw new Error(`Failed to decode user information from JWT: ${error.message}`);
292
+ } else {
293
+ throw new Error('Failed to decode user information from JWT: Unknown error');
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Refreshes the access token using the stored refresh token.
300
+ *
301
+ * @returns Promise<SignInWithYouVersionResult | null> - New tokens if refresh succeeds, null otherwise
302
+ * @throws An error if refresh fails or no refresh token is available
303
+ */
304
+ static async refreshTokens(): Promise<SignInWithYouVersionResult | null> {
305
+ const refreshToken = YouVersionPlatformConfiguration.refreshToken;
306
+ const appKey = YouVersionPlatformConfiguration.appKey;
307
+ const existingIdToken = YouVersionPlatformConfiguration.idToken;
308
+
309
+ if (!refreshToken || !existingIdToken) {
310
+ throw new Error('No refresh token or id token available');
311
+ }
312
+
313
+ if (!appKey) {
314
+ throw new Error(
315
+ 'YouVersionPlatformConfiguration.appKey must be set before refreshing tokens',
85
316
  );
86
317
  }
87
318
 
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
- }
319
+ try {
320
+ const url = new URL(`https://${YouVersionPlatformConfiguration.apiHost}/auth/token`);
321
+
322
+ const parameters = new URLSearchParams({
323
+ grant_type: 'refresh_token',
324
+ refresh_token: refreshToken,
325
+ client_id: appKey,
326
+ });
327
+
328
+ const request = new Request(url, {
329
+ method: 'POST',
330
+ body: parameters,
331
+ headers: {
332
+ 'Content-Type': 'application/x-www-form-urlencoded',
333
+ },
334
+ });
335
+
336
+ const response = await fetch(request);
337
+
338
+ if (!response.ok) {
339
+ throw new Error(`Token refresh failed: ${response.status} ${response.statusText}`);
340
+ }
341
+
342
+ const tokens = (await response.json()) as {
343
+ access_token: string;
344
+ expires_in: number;
345
+ refresh_token: string;
346
+ scope: string;
347
+ token_type: string;
348
+ };
349
+
350
+ // Create result with new tokens but preserve user info
351
+ const result = new SignInWithYouVersionResult({
352
+ accessToken: tokens.access_token,
353
+ expiresIn: tokens.expires_in,
354
+ refreshToken: tokens.refresh_token,
355
+ idToken: existingIdToken,
356
+ permissions: tokens.scope
357
+ .split(' ')
358
+ .map((p) => p.trim())
359
+ .filter((p) => p.length > 0)
360
+ .filter((p): p is SignInWithYouVersionPermissionValues =>
361
+ Object.values(SignInWithYouVersionPermission).includes(
362
+ p as SignInWithYouVersionPermissionValues,
363
+ ),
364
+ ),
365
+ });
366
+
367
+ // Store updated tokens
368
+ YouVersionPlatformConfiguration.saveAuthData(
369
+ result.accessToken || null,
370
+ result.refreshToken || null,
371
+ result.idToken || null,
372
+ result.expiryDate || null,
373
+ );
374
+
375
+ return result;
376
+ } catch (error) {
377
+ if (error instanceof Error) {
378
+ throw new Error(`Token refresh failed: ${error.message}`);
379
+ } else {
380
+ throw new Error('Token refresh failed: Unknown error');
132
381
  }
133
382
  }
383
+ }
134
384
 
135
- throw lastError || new Error('Failed to retrieve user information after multiple attempts');
385
+ /**
386
+ * Checks if the current access token is expired or about to expire.
387
+ *
388
+ * @returns true if token is expired or about to expire
389
+ */
390
+ static isTokenExpired(): boolean {
391
+ const expiryDate = YouVersionPlatformConfiguration.tokenExpiryDate;
392
+ if (!expiryDate) {
393
+ return true; // No expiry date means no token or invalid token
394
+ }
395
+
396
+ return new Date().getTime() >= expiryDate.getTime();
397
+ }
398
+
399
+ /**
400
+ * Refreshes the access token if it's expired or about to expire.
401
+ *
402
+ * @returns Promise<boolean> - true if refresh was successful or not needed, false if failed
403
+ */
404
+ static async refreshTokenIfNeeded(): Promise<boolean> {
405
+ if (!this.isTokenExpired()) {
406
+ return true; // Token is still valid
407
+ }
408
+
409
+ try {
410
+ const result = await this.refreshTokens();
411
+ return !!result;
412
+ } catch {
413
+ // Refresh failed, clear tokens
414
+ YouVersionPlatformConfiguration.clearAuthTokens();
415
+ return false;
416
+ }
136
417
  }
137
418
  }
@@ -1,12 +1,16 @@
1
- import type { YouVersionUserInfo } from './YouVersionUserInfo';
2
-
1
+ /**
2
+ * Security Note: Tokens are stored in localStorage for persistence.
3
+ * Ensure your application follows XSS prevention best practices:
4
+ * - Sanitize user input
5
+ * - Use Content Security Policy headers
6
+ * - Avoid innerHTML with untrusted content
7
+ */
3
8
  export class YouVersionPlatformConfiguration {
4
9
  private static _appKey: string | null = null;
5
10
  private static _installationId: string | null = null;
6
- private static _accessToken: string | null = null;
7
11
  private static _apiHost: string = 'api.youversion.com';
8
- private static _isPreviewMode: boolean = false;
9
- private static _previewUserInfo: YouVersionUserInfo | null = null;
12
+ private static _refreshTokenKey: string | null = null;
13
+ private static _expiryDateKey: string | null = null;
10
14
 
11
15
  private static getOrSetInstallationId(): string {
12
16
  if (typeof window === 'undefined') {
@@ -23,6 +27,58 @@ export class YouVersionPlatformConfiguration {
23
27
  return newId;
24
28
  }
25
29
 
30
+ public static saveAuthData(
31
+ accessToken: string | null,
32
+ refreshToken: string | null,
33
+ idToken: string | null,
34
+ expiryDate: Date | null,
35
+ ): void {
36
+ if (accessToken !== null) {
37
+ localStorage.setItem('accessToken', accessToken);
38
+ } else {
39
+ localStorage.removeItem('accessToken');
40
+ }
41
+
42
+ if (refreshToken !== null) {
43
+ localStorage.setItem('refreshToken', refreshToken);
44
+ } else {
45
+ localStorage.removeItem('refreshToken');
46
+ }
47
+
48
+ if (idToken !== null) {
49
+ localStorage.setItem('idToken', idToken);
50
+ } else {
51
+ localStorage.removeItem('idToken');
52
+ }
53
+
54
+ if (expiryDate !== null) {
55
+ localStorage.setItem('expiryDate', expiryDate.toISOString());
56
+ } else {
57
+ localStorage.removeItem('expiryDate');
58
+ }
59
+ }
60
+
61
+ public static clearAuthTokens(): void {
62
+ this.saveAuthData(null, null, null, null);
63
+ }
64
+
65
+ public static get accessToken(): string | null {
66
+ return localStorage.getItem('accessToken');
67
+ }
68
+
69
+ public static get refreshToken(): string | null {
70
+ return localStorage.getItem('refreshToken');
71
+ }
72
+
73
+ public static get idToken(): string | null {
74
+ return localStorage.getItem('idToken');
75
+ }
76
+
77
+ public static get tokenExpiryDate(): Date | null {
78
+ const dateString = localStorage.getItem('expiryDate');
79
+ return dateString ? new Date(dateString) : null;
80
+ }
81
+
26
82
  static get appKey(): string | null {
27
83
  return this._appKey;
28
84
  }
@@ -42,18 +98,6 @@ export class YouVersionPlatformConfiguration {
42
98
  this._installationId = value || this.getOrSetInstallationId();
43
99
  }
44
100
 
45
- static setAccessToken(token: string | null): void {
46
- // Validate token: if not null, must be a non-empty string
47
- if (token !== null && (typeof token !== 'string' || token.trim().length === 0)) {
48
- throw new Error('Access token must be a non-empty string or null');
49
- }
50
- this._accessToken = token;
51
- }
52
-
53
- static get accessToken(): string | null {
54
- return this._accessToken;
55
- }
56
-
57
101
  static get apiHost(): string {
58
102
  return this._apiHost;
59
103
  }
@@ -62,19 +106,19 @@ export class YouVersionPlatformConfiguration {
62
106
  this._apiHost = value;
63
107
  }
64
108
 
65
- static get isPreviewMode(): boolean {
66
- return this._isPreviewMode;
109
+ static get refreshTokenKey(): string | null {
110
+ return this._refreshTokenKey;
67
111
  }
68
112
 
69
- static set isPreviewMode(value: boolean) {
70
- this._isPreviewMode = value;
113
+ static set refreshTokenKey(value: string) {
114
+ this._refreshTokenKey = value;
71
115
  }
72
116
 
73
- static get previewUserInfo(): YouVersionUserInfo | null {
74
- return this._previewUserInfo;
117
+ static get expiryDateKey(): string | null {
118
+ return this._expiryDateKey;
75
119
  }
76
120
 
77
- static set previewUserInfo(value: YouVersionUserInfo | null) {
78
- this._previewUserInfo = value;
121
+ static set expiryDateKey(value: string) {
122
+ this._expiryDateKey = value;
79
123
  }
80
124
  }
@@ -1,14 +1,14 @@
1
1
  export interface YouVersionUserInfoJSON {
2
- first_name?: string;
3
- last_name?: string;
2
+ name?: string;
4
3
  id?: string;
5
4
  avatar_url?: string;
5
+ email?: string;
6
6
  }
7
7
 
8
8
  export class YouVersionUserInfo {
9
- readonly firstName?: string;
10
- readonly lastName?: string;
9
+ readonly name?: string;
11
10
  readonly userId?: string;
11
+ readonly email?: string;
12
12
  readonly avatarUrlFormat?: string;
13
13
 
14
14
  constructor(data: YouVersionUserInfoJSON) {
@@ -16,9 +16,9 @@ export class YouVersionUserInfo {
16
16
  throw new Error('Invalid user data provided');
17
17
  }
18
18
 
19
- this.firstName = data.first_name;
20
- this.lastName = data.last_name;
19
+ this.name = data.name;
21
20
  this.userId = data.id;
21
+ this.email = data.email;
22
22
  this.avatarUrlFormat = data.avatar_url;
23
23
  }
24
24