react-native-nitro-auth 0.6.1 → 0.6.3

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
@@ -24,6 +24,16 @@ const WEB_STORAGE_MODES = new Set([
24
24
  STORAGE_MODE_LOCAL,
25
25
  STORAGE_MODE_MEMORY,
26
26
  ] as const);
27
+ const MICROSOFT_TENANT_PATTERN =
28
+ /^(common|organizations|consumers|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9][A-Za-z0-9._-]{0,127})$/;
29
+ const MICROSOFT_B2C_TENANT_PATH_PATTERN =
30
+ /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[A-Za-z0-9][A-Za-z0-9._-]{0,127})\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
31
+ const MICROSOFT_B2C_POLICY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
32
+ const MICROSOFT_B2CLOGIN_DOMAIN_SUFFIX = ".b2clogin.com";
33
+ const MICROSOFT_B2CLOGIN_TENANT_NAME_PATTERN =
34
+ /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
35
+ const MICROSOFT_DOMAIN_PATTERN =
36
+ /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/;
27
37
  const WEB_AUTH_ERROR_CODES: ReadonlySet<string> = new Set<AuthErrorCode>([
28
38
  "cancelled",
29
39
  "timeout",
@@ -1328,15 +1338,68 @@ class AuthWeb implements Auth {
1328
1338
  }
1329
1339
 
1330
1340
  private getMicrosoftAuthBaseUrl(tenant: string, b2cDomain?: string): string {
1331
- if (tenant.startsWith("https://")) {
1332
- return tenant.endsWith("/") ? tenant : `${tenant}/`;
1341
+ const trimmedTenant = tenant.trim();
1342
+
1343
+ const trimmedDomain = b2cDomain?.trim().toLowerCase();
1344
+ if (trimmedDomain) {
1345
+ if (!MICROSOFT_DOMAIN_PATTERN.test(trimmedDomain)) {
1346
+ throw new AuthWebError(
1347
+ "configuration_error",
1348
+ "Microsoft B2C domain must be a hostname.",
1349
+ );
1350
+ }
1351
+ const b2cTenantPath = this.getMicrosoftB2cTenantPath(
1352
+ trimmedTenant,
1353
+ trimmedDomain,
1354
+ );
1355
+ return `https://${trimmedDomain}/${b2cTenantPath}/`;
1356
+ }
1357
+
1358
+ if (!MICROSOFT_TENANT_PATTERN.test(trimmedTenant)) {
1359
+ throw new AuthWebError(
1360
+ "configuration_error",
1361
+ "Microsoft tenant must be common, organizations, consumers, a tenant ID, or tenant domain.",
1362
+ );
1333
1363
  }
1334
1364
 
1335
- if (b2cDomain) {
1336
- return `https://${b2cDomain}/tfp/${tenant}/`;
1337
- } else {
1338
- return `https://login.microsoftonline.com/${tenant}/`;
1365
+ return `https://login.microsoftonline.com/${trimmedTenant}/`;
1366
+ }
1367
+
1368
+ private getMicrosoftB2cTenantPath(tenant: string, domain: string): string {
1369
+ if (MICROSOFT_B2C_TENANT_PATH_PATTERN.test(tenant)) {
1370
+ return tenant;
1339
1371
  }
1372
+
1373
+ if (!MICROSOFT_B2C_POLICY_PATTERN.test(tenant)) {
1374
+ throw new AuthWebError(
1375
+ "configuration_error",
1376
+ "Microsoft B2C tenant must be a policy or tenant/policy path.",
1377
+ );
1378
+ }
1379
+
1380
+ const tenantName = this.getMicrosoftB2cTenantName(domain);
1381
+ if (tenantName === undefined) {
1382
+ throw new AuthWebError(
1383
+ "configuration_error",
1384
+ "Microsoft B2C custom domains require microsoftTenant as a tenant/policy path.",
1385
+ );
1386
+ }
1387
+
1388
+ return `${tenantName}.onmicrosoft.com/${tenant}`;
1389
+ }
1390
+
1391
+ private getMicrosoftB2cTenantName(domain: string): string | undefined {
1392
+ if (!domain.endsWith(MICROSOFT_B2CLOGIN_DOMAIN_SUFFIX)) {
1393
+ return undefined;
1394
+ }
1395
+
1396
+ const tenantName = domain.slice(
1397
+ 0,
1398
+ -MICROSOFT_B2CLOGIN_DOMAIN_SUFFIX.length,
1399
+ );
1400
+ return MICROSOFT_B2CLOGIN_TENANT_NAME_PATTERN.test(tenantName)
1401
+ ? tenantName
1402
+ : undefined;
1340
1403
  }
1341
1404
 
1342
1405
  private decodeMicrosoftJwt(token: string): Partial<AuthUser> {
@@ -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: {