react-native-nitro-auth 0.6.2 → 0.6.4

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/Auth.web.ts CHANGED
@@ -86,9 +86,12 @@ type AuthWebExtraConfig = {
86
86
  };
87
87
 
88
88
  type JsonObject = Record<string, unknown>;
89
+ type AppleAuthInitConfig = Parameters<
90
+ NonNullable<Window["AppleID"]>["auth"]["init"]
91
+ >[0];
89
92
 
90
93
  class AuthWebError extends Error {
91
- public readonly underlyingError?: string;
94
+ public readonly underlyingError: string | undefined;
92
95
 
93
96
  constructor(message: AuthErrorCode, underlyingError?: string) {
94
97
  super(message);
@@ -121,6 +124,29 @@ const getOptionalNumber = (
121
124
  : undefined;
122
125
  };
123
126
 
127
+ function setIfDefined<T extends object, K extends keyof T>(
128
+ target: T,
129
+ key: K,
130
+ value: T[K] | undefined,
131
+ ): void {
132
+ if (value !== undefined) {
133
+ target[key] = value;
134
+ }
135
+ }
136
+
137
+ function setOrDelete<T extends object, K extends keyof T>(
138
+ target: T,
139
+ key: K,
140
+ value: T[K] | undefined,
141
+ ): void {
142
+ if (value === undefined) {
143
+ delete target[key];
144
+ return;
145
+ }
146
+
147
+ target[key] = value;
148
+ }
149
+
124
150
  const parseExpiresInSeconds = (value: unknown): number | undefined => {
125
151
  const candidate =
126
152
  typeof value === "number"
@@ -152,19 +178,32 @@ const parseAuthUser = (value: unknown): AuthUser | undefined => {
152
178
  )
153
179
  : undefined;
154
180
 
155
- return {
181
+ const user: AuthUser = {
156
182
  provider: value.provider,
157
- email: getOptionalString(value, "email"),
158
- name: getOptionalString(value, "name"),
159
- photo: getOptionalString(value, "photo"),
160
- idToken: getOptionalString(value, "idToken"),
161
- accessToken: getOptionalString(value, "accessToken"),
162
- refreshToken: getOptionalString(value, "refreshToken"),
163
- serverAuthCode: getOptionalString(value, "serverAuthCode"),
164
- scopes,
165
- expirationTime: getOptionalNumber(value, "expirationTime"),
166
- underlyingError: getOptionalString(value, "underlyingError"),
167
183
  };
184
+ setIfDefined(user, "email", getOptionalString(value, "email"));
185
+ setIfDefined(user, "name", getOptionalString(value, "name"));
186
+ setIfDefined(user, "photo", getOptionalString(value, "photo"));
187
+ setIfDefined(user, "idToken", getOptionalString(value, "idToken"));
188
+ setIfDefined(user, "accessToken", getOptionalString(value, "accessToken"));
189
+ setIfDefined(user, "refreshToken", getOptionalString(value, "refreshToken"));
190
+ setIfDefined(
191
+ user,
192
+ "serverAuthCode",
193
+ getOptionalString(value, "serverAuthCode"),
194
+ );
195
+ setIfDefined(user, "scopes", scopes);
196
+ setIfDefined(
197
+ user,
198
+ "expirationTime",
199
+ getOptionalNumber(value, "expirationTime"),
200
+ );
201
+ setIfDefined(
202
+ user,
203
+ "underlyingError",
204
+ getOptionalString(value, "underlyingError"),
205
+ );
206
+ return user;
168
207
  };
169
208
 
170
209
  const parseScopes = (value: unknown): string[] | undefined => {
@@ -191,18 +230,41 @@ const parseAuthWebExtraConfig = (value: unknown): AuthWebExtraConfig => {
191
230
  ? nitroAuthWebStorageCandidate
192
231
  : undefined;
193
232
 
194
- return {
195
- googleWebClientId: getOptionalString(value, "googleWebClientId"),
196
- microsoftClientId: getOptionalString(value, "microsoftClientId"),
197
- microsoftTenant: getOptionalString(value, "microsoftTenant"),
198
- microsoftB2cDomain: getOptionalString(value, "microsoftB2cDomain"),
199
- appleWebClientId: getOptionalString(value, "appleWebClientId"),
200
- nitroAuthWebStorage,
201
- nitroAuthPersistTokensOnWeb:
202
- typeof value.nitroAuthPersistTokensOnWeb === "boolean"
203
- ? value.nitroAuthPersistTokensOnWeb
204
- : undefined,
205
- };
233
+ const config: AuthWebExtraConfig = {};
234
+ setIfDefined(
235
+ config,
236
+ "googleWebClientId",
237
+ getOptionalString(value, "googleWebClientId"),
238
+ );
239
+ setIfDefined(
240
+ config,
241
+ "microsoftClientId",
242
+ getOptionalString(value, "microsoftClientId"),
243
+ );
244
+ setIfDefined(
245
+ config,
246
+ "microsoftTenant",
247
+ getOptionalString(value, "microsoftTenant"),
248
+ );
249
+ setIfDefined(
250
+ config,
251
+ "microsoftB2cDomain",
252
+ getOptionalString(value, "microsoftB2cDomain"),
253
+ );
254
+ setIfDefined(
255
+ config,
256
+ "appleWebClientId",
257
+ getOptionalString(value, "appleWebClientId"),
258
+ );
259
+ setIfDefined(config, "nitroAuthWebStorage", nitroAuthWebStorage);
260
+ setIfDefined(
261
+ config,
262
+ "nitroAuthPersistTokensOnWeb",
263
+ typeof value.nitroAuthPersistTokensOnWeb === "boolean"
264
+ ? value.nitroAuthPersistTokensOnWeb
265
+ : undefined,
266
+ );
267
+ return config;
206
268
  };
207
269
 
208
270
  const getConfig = (): AuthWebExtraConfig => {
@@ -723,20 +785,23 @@ class AuthWeb implements Auth {
723
785
  : {};
724
786
  const user: AuthUser = {
725
787
  ...currentUser,
726
- idToken: effectiveIdToken,
727
- accessToken: accessToken ?? undefined,
728
- refreshToken: newRefreshToken ?? currentUser.refreshToken,
729
- expirationTime,
730
788
  ...claims,
731
789
  };
790
+ setOrDelete(user, "idToken", effectiveIdToken);
791
+ setOrDelete(user, "accessToken", accessToken);
792
+ setOrDelete(
793
+ user,
794
+ "refreshToken",
795
+ newRefreshToken ?? currentUser.refreshToken,
796
+ );
797
+ setOrDelete(user, "expirationTime", expirationTime);
732
798
  this.updateUser(user);
733
799
 
734
- const tokens: AuthTokens = {
735
- accessToken: accessToken ?? undefined,
736
- idToken: effectiveIdToken,
737
- refreshToken: newRefreshToken ?? undefined,
738
- expirationTime,
739
- };
800
+ const tokens: AuthTokens = {};
801
+ setIfDefined(tokens, "accessToken", accessToken);
802
+ setIfDefined(tokens, "idToken", effectiveIdToken);
803
+ setIfDefined(tokens, "refreshToken", newRefreshToken);
804
+ setIfDefined(tokens, "expirationTime", expirationTime);
740
805
  this.notifyTokenListeners(tokens);
741
806
  return tokens;
742
807
  }
@@ -757,12 +822,11 @@ class AuthWeb implements Auth {
757
822
  ),
758
823
  );
759
824
  this.assertActiveGeneration(generation);
760
- const tokens: AuthTokens = {
761
- accessToken: this._currentUser.accessToken,
762
- idToken: this._currentUser.idToken,
763
- refreshToken: this._currentUser.refreshToken,
764
- expirationTime: this._currentUser.expirationTime,
765
- };
825
+ const tokens: AuthTokens = {};
826
+ setIfDefined(tokens, "accessToken", this._currentUser.accessToken);
827
+ setIfDefined(tokens, "idToken", this._currentUser.idToken);
828
+ setIfDefined(tokens, "refreshToken", this._currentUser.refreshToken);
829
+ setIfDefined(tokens, "expirationTime", this._currentUser.expirationTime);
766
830
  this.notifyTokenListeners(tokens);
767
831
  return tokens;
768
832
  }
@@ -1084,14 +1148,14 @@ class AuthWeb implements Auth {
1084
1148
  const user: AuthUser = {
1085
1149
  provider: "google",
1086
1150
  idToken,
1087
- accessToken: accessToken ?? undefined,
1088
- serverAuthCode: code ?? undefined,
1089
- userId: getOptionalString(decoded, "sub"),
1090
- hostedDomain: getOptionalString(decoded, "hd"),
1091
1151
  scopes,
1092
- expirationTime: this.getExpirationTime(expiresIn),
1093
1152
  ...this.decodeGoogleJwt(idToken),
1094
1153
  };
1154
+ setIfDefined(user, "accessToken", accessToken ?? undefined);
1155
+ setIfDefined(user, "serverAuthCode", code ?? undefined);
1156
+ setIfDefined(user, "userId", getOptionalString(decoded, "sub"));
1157
+ setIfDefined(user, "hostedDomain", getOptionalString(decoded, "hd"));
1158
+ setIfDefined(user, "expirationTime", this.getExpirationTime(expiresIn));
1095
1159
  this.updateUser(user);
1096
1160
  })
1097
1161
  .then(() => {
@@ -1106,13 +1170,13 @@ class AuthWeb implements Auth {
1106
1170
  private decodeGoogleJwt(token: string): Partial<AuthUser> {
1107
1171
  try {
1108
1172
  const decoded = this.parseJwtPayload(token);
1109
- return {
1110
- email: getOptionalString(decoded, "email"),
1111
- name: getOptionalString(decoded, "name"),
1112
- photo: getOptionalString(decoded, "picture"),
1113
- userId: getOptionalString(decoded, "sub"),
1114
- hostedDomain: getOptionalString(decoded, "hd"),
1115
- };
1173
+ const user: Partial<AuthUser> = {};
1174
+ setIfDefined(user, "email", getOptionalString(decoded, "email"));
1175
+ setIfDefined(user, "name", getOptionalString(decoded, "name"));
1176
+ setIfDefined(user, "photo", getOptionalString(decoded, "picture"));
1177
+ setIfDefined(user, "userId", getOptionalString(decoded, "sub"));
1178
+ setIfDefined(user, "hostedDomain", getOptionalString(decoded, "hd"));
1179
+ return user;
1116
1180
  } catch (error) {
1117
1181
  logger.warn("Failed to decode Google ID token", { error: String(error) });
1118
1182
  return {};
@@ -1328,12 +1392,16 @@ class AuthWeb implements Auth {
1328
1392
  const user: AuthUser = {
1329
1393
  provider: "microsoft",
1330
1394
  idToken,
1331
- accessToken: accessToken ?? undefined,
1332
- refreshToken: refreshToken ?? undefined,
1333
1395
  scopes,
1334
- expirationTime: this.getExpirationTime(json["expires_in"]),
1335
1396
  ...claims,
1336
1397
  };
1398
+ setIfDefined(user, "accessToken", accessToken);
1399
+ setIfDefined(user, "refreshToken", refreshToken);
1400
+ setIfDefined(
1401
+ user,
1402
+ "expirationTime",
1403
+ this.getExpirationTime(json["expires_in"]),
1404
+ );
1337
1405
  this.updateUser(user);
1338
1406
  }
1339
1407
 
@@ -1405,12 +1473,15 @@ class AuthWeb implements Auth {
1405
1473
  private decodeMicrosoftJwt(token: string): Partial<AuthUser> {
1406
1474
  try {
1407
1475
  const decoded = this.parseJwtPayload(token);
1408
- return {
1409
- email:
1410
- getOptionalString(decoded, "preferred_username") ??
1476
+ const user: Partial<AuthUser> = {};
1477
+ setIfDefined(
1478
+ user,
1479
+ "email",
1480
+ getOptionalString(decoded, "preferred_username") ??
1411
1481
  getOptionalString(decoded, "email"),
1412
- name: getOptionalString(decoded, "name"),
1413
- };
1482
+ );
1483
+ setIfDefined(user, "name", getOptionalString(decoded, "name"));
1484
+ return user;
1414
1485
  } catch (error) {
1415
1486
  logger.warn("Failed to decode Microsoft ID token", {
1416
1487
  error: String(error),
@@ -1495,16 +1566,17 @@ class AuthWeb implements Auth {
1495
1566
  throw new Error("Apple SDK not loaded");
1496
1567
  }
1497
1568
 
1498
- window.AppleID.auth.init({
1569
+ const appleAuthConfig: AppleAuthInitConfig = {
1499
1570
  clientId,
1500
1571
  scope: (options?.scopes?.length
1501
1572
  ? options.scopes
1502
1573
  : ["name", "email"]
1503
1574
  ).join(" "),
1504
1575
  redirectURI: window.location.origin,
1505
- nonce: options?.nonce,
1506
1576
  usePopup: true,
1507
- });
1577
+ };
1578
+ setIfDefined(appleAuthConfig, "nonce", options?.nonce);
1579
+ window.AppleID.auth.init(appleAuthConfig);
1508
1580
 
1509
1581
  try {
1510
1582
  const response: AppleAuthResponse = await window.AppleID.auth.signIn();
@@ -1514,12 +1586,16 @@ class AuthWeb implements Auth {
1514
1586
  const user: AuthUser = {
1515
1587
  provider: "apple",
1516
1588
  idToken: response.authorization.id_token,
1517
- authorizationCode: response.authorization.code,
1518
- email: response.user?.email,
1519
- name: response.user?.name
1589
+ };
1590
+ setIfDefined(user, "authorizationCode", response.authorization.code);
1591
+ setIfDefined(user, "email", response.user?.email);
1592
+ setIfDefined(
1593
+ user,
1594
+ "name",
1595
+ response.user?.name
1520
1596
  ? `${response.user.name.firstName} ${response.user.name.lastName}`.trim()
1521
1597
  : undefined,
1522
- };
1598
+ );
1523
1599
  this.updateUser(user);
1524
1600
  } catch (error) {
1525
1601
  throw this.mapError(error);
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useCallback, useMemo, useState } from "react";
2
2
  import type { ViewStyle, TextStyle } from "react-native";
3
3
  import {
4
4
  Pressable,
@@ -34,7 +34,7 @@ const PROVIDER_LABELS: Record<AuthProvider, string> = {
34
34
  const PROVIDER_PRIMARY_BACKGROUND: Record<AuthProvider, string> = {
35
35
  google: "#4285F4",
36
36
  apple: "#000000",
37
- microsoft: "#2F2F2F",
37
+ microsoft: "#1f2937",
38
38
  };
39
39
 
40
40
  const getBackgroundColor = ({
@@ -46,21 +46,29 @@ const getBackgroundColor = ({
46
46
  variant: SocialButtonVariant;
47
47
  provider: AuthProvider;
48
48
  }): string => {
49
- if (disabled) return "#CCCCCC";
49
+ if (disabled) return "#E2E8F0";
50
50
  if (variant === "black") return "#000000";
51
51
  if (variant === "white") return "#FFFFFF";
52
52
  if (variant === "outline") return "transparent";
53
53
  return PROVIDER_PRIMARY_BACKGROUND[provider];
54
54
  };
55
55
 
56
- const getTextColor = (variant: SocialButtonVariant): string =>
57
- variant === "white" || variant === "outline" ? "#000000" : "#FFFFFF";
56
+ const getTextColor = ({
57
+ disabled,
58
+ variant,
59
+ }: {
60
+ disabled: boolean;
61
+ variant: SocialButtonVariant;
62
+ }): string => {
63
+ if (disabled) return "#64748B";
64
+ return variant === "white" || variant === "outline" ? "#111827" : "#FFFFFF";
65
+ };
58
66
 
59
67
  async function performLogin(provider: AuthProvider): Promise<void> {
60
68
  await AuthService.login(provider);
61
69
  }
62
70
 
63
- export const SocialButton = ({
71
+ export const SocialButton = React.memo(function SocialButton({
64
72
  provider,
65
73
  variant = "primary",
66
74
  borderRadius = 8,
@@ -70,10 +78,11 @@ export const SocialButton = ({
70
78
  onSuccess,
71
79
  onError,
72
80
  onPress,
73
- }: SocialButtonProps) => {
81
+ }: SocialButtonProps) {
74
82
  const [loading, setLoading] = useState(false);
83
+ const isDisabled = loading || disabled === true;
75
84
 
76
- const handleLogin = async () => {
85
+ const handleLogin = useCallback(async () => {
77
86
  if (loading || disabled) return;
78
87
  if (onPress) {
79
88
  onPress();
@@ -96,36 +105,41 @@ export const SocialButton = ({
96
105
  } finally {
97
106
  setLoading(false);
98
107
  }
99
- };
100
- const isDisabled = loading || disabled === true;
108
+ }, [disabled, loading, onError, onPress, onSuccess, provider]);
101
109
 
102
- const getBorderColor = () => {
103
- if (variant === "outline") return "#DDDDDD";
104
- return "transparent";
105
- };
110
+ const buttonStyle = useMemo(
111
+ () => ({
112
+ backgroundColor: getBackgroundColor({
113
+ disabled: isDisabled,
114
+ variant,
115
+ provider,
116
+ }),
117
+ borderRadius,
118
+ borderColor: variant === "outline" ? "#DDDDDD" : "transparent",
119
+ borderWidth: variant === "outline" ? 1 : 0,
120
+ }),
121
+ [borderRadius, isDisabled, provider, variant],
122
+ );
123
+
124
+ const textColor = getTextColor({ disabled: isDisabled, variant });
125
+ const labelStyle = useMemo(
126
+ () => [styles.text, { color: textColor }, textStyle],
127
+ [textColor, textStyle],
128
+ );
129
+ const appleIconStyle = useMemo(
130
+ () => [styles.iconText, { color: textColor }],
131
+ [textColor],
132
+ );
106
133
 
107
134
  return (
108
135
  <Pressable
109
- style={[
110
- styles.button,
111
- {
112
- backgroundColor: getBackgroundColor({
113
- disabled: isDisabled,
114
- variant,
115
- provider,
116
- }),
117
- borderRadius,
118
- borderColor: getBorderColor(),
119
- borderWidth: variant === "outline" ? 1 : 0,
120
- },
121
- style,
122
- ]}
136
+ style={[styles.button, buttonStyle, style]}
123
137
  onPress={handleLogin}
124
138
  disabled={isDisabled}
125
139
  >
126
140
  <View style={styles.content}>
127
141
  {loading ? (
128
- <ActivityIndicator size="small" color={getTextColor(variant)} />
142
+ <ActivityIndicator size="small" color={textColor} />
129
143
  ) : (
130
144
  <>
131
145
  {provider === "google" && variant !== "primary" && (
@@ -135,11 +149,7 @@ export const SocialButton = ({
135
149
  )}
136
150
  {provider === "apple" && variant !== "primary" && (
137
151
  <View style={styles.iconPlaceholder}>
138
- <Text
139
- style={[styles.iconText, { color: getTextColor(variant) }]}
140
- >
141
-
142
- </Text>
152
+ <Text style={appleIconStyle}></Text>
143
153
  </View>
144
154
  )}
145
155
  {provider === "microsoft" && variant !== "primary" && (
@@ -147,9 +157,7 @@ export const SocialButton = ({
147
157
  <Text style={styles.microsoftIconText}>⊞</Text>
148
158
  </View>
149
159
  )}
150
- <Text
151
- style={[styles.text, { color: getTextColor(variant) }, textStyle]}
152
- >
160
+ <Text style={labelStyle}>
153
161
  Sign in with {PROVIDER_LABELS[provider]}
154
162
  </Text>
155
163
  </>
@@ -157,7 +165,7 @@ export const SocialButton = ({
157
165
  </View>
158
166
  </Pressable>
159
167
  );
160
- };
168
+ });
161
169
 
162
170
  const styles = StyleSheet.create({
163
171
  button: {