@terreno/ui 0.1.0 → 0.2.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.
Files changed (64) hide show
  1. package/dist/Common.d.ts +44 -0
  2. package/dist/SocialLoginButton.d.ts +19 -0
  3. package/dist/SocialLoginButton.js +119 -0
  4. package/dist/SocialLoginButton.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/login/LoginScreen.d.ts +25 -0
  9. package/dist/login/LoginScreen.js +55 -0
  10. package/dist/login/LoginScreen.js.map +1 -0
  11. package/dist/login/index.d.ts +2 -0
  12. package/dist/login/index.js +2 -0
  13. package/dist/login/index.js.map +1 -0
  14. package/dist/login/loginTypes.d.ts +48 -0
  15. package/dist/login/loginTypes.js +2 -0
  16. package/dist/login/loginTypes.js.map +1 -0
  17. package/dist/signUp/OAuthButtons.d.ts +18 -0
  18. package/dist/signUp/OAuthButtons.js +15 -0
  19. package/dist/signUp/OAuthButtons.js.map +1 -0
  20. package/dist/signUp/PasswordRequirements.d.ts +15 -0
  21. package/dist/signUp/PasswordRequirements.js +14 -0
  22. package/dist/signUp/PasswordRequirements.js.map +1 -0
  23. package/dist/signUp/SignUpScreen.d.ts +26 -0
  24. package/dist/signUp/SignUpScreen.js +64 -0
  25. package/dist/signUp/SignUpScreen.js.map +1 -0
  26. package/dist/signUp/Swiper.d.ts +13 -0
  27. package/dist/signUp/Swiper.js +16 -0
  28. package/dist/signUp/Swiper.js.map +1 -0
  29. package/dist/signUp/index.d.ts +6 -0
  30. package/dist/signUp/index.js +6 -0
  31. package/dist/signUp/index.js.map +1 -0
  32. package/dist/signUp/passwordPresets.d.ts +9 -0
  33. package/dist/signUp/passwordPresets.js +41 -0
  34. package/dist/signUp/passwordPresets.js.map +1 -0
  35. package/dist/signUp/signUpTypes.d.ts +90 -0
  36. package/dist/signUp/signUpTypes.js +2 -0
  37. package/dist/signUp/signUpTypes.js.map +1 -0
  38. package/package.json +3 -2
  39. package/src/Common.ts +52 -0
  40. package/src/SocialLoginButton.test.tsx +158 -0
  41. package/src/SocialLoginButton.tsx +182 -0
  42. package/src/__snapshots__/SocialLoginButton.test.tsx.snap +277 -0
  43. package/src/index.tsx +4 -0
  44. package/src/login/LoginScreen.test.tsx +148 -0
  45. package/src/login/LoginScreen.tsx +159 -0
  46. package/src/login/__snapshots__/LoginScreen.test.tsx.snap +630 -0
  47. package/src/login/index.ts +2 -0
  48. package/src/login/loginTypes.ts +51 -0
  49. package/src/signUp/OAuthButtons.test.tsx +45 -0
  50. package/src/signUp/OAuthButtons.tsx +52 -0
  51. package/src/signUp/PasswordRequirements.test.tsx +41 -0
  52. package/src/signUp/PasswordRequirements.tsx +49 -0
  53. package/src/signUp/SignUpScreen.test.tsx +134 -0
  54. package/src/signUp/SignUpScreen.tsx +172 -0
  55. package/src/signUp/Swiper.test.tsx +46 -0
  56. package/src/signUp/Swiper.tsx +59 -0
  57. package/src/signUp/__snapshots__/OAuthButtons.test.tsx.snap +272 -0
  58. package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +427 -0
  59. package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +851 -0
  60. package/src/signUp/__snapshots__/Swiper.test.tsx.snap +249 -0
  61. package/src/signUp/index.ts +13 -0
  62. package/src/signUp/passwordPresets.test.ts +57 -0
  63. package/src/signUp/passwordPresets.ts +43 -0
  64. package/src/signUp/signUpTypes.ts +94 -0
@@ -0,0 +1,6 @@
1
+ export { OAuthButtons } from "./OAuthButtons";
2
+ export { PasswordRequirements } from "./PasswordRequirements";
3
+ export { defaultPasswordRequirements, simplePasswordRequirements } from "./passwordPresets";
4
+ export { SignUpScreen } from "./SignUpScreen";
5
+ export { Swiper } from "./Swiper";
6
+ export type { OAuthProvider, OAuthProviderConfig, OnboardingPage, PasswordRequirement, SignUpFieldConfig, SignUpScreenProps, } from "./signUpTypes";
@@ -0,0 +1,6 @@
1
+ export { OAuthButtons } from "./OAuthButtons";
2
+ export { PasswordRequirements } from "./PasswordRequirements";
3
+ export { defaultPasswordRequirements, simplePasswordRequirements } from "./passwordPresets";
4
+ export { SignUpScreen } from "./SignUpScreen";
5
+ export { Swiper } from "./Swiper";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/signUp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAC,2BAA2B,EAAE,0BAA0B,EAAC,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,MAAM,EAAC,MAAM,UAAU,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { PasswordRequirement } from "./signUpTypes";
2
+ /**
3
+ * Default password requirements with strong validation rules.
4
+ */
5
+ export declare const defaultPasswordRequirements: PasswordRequirement[];
6
+ /**
7
+ * Simple password requirements with minimal validation.
8
+ */
9
+ export declare const simplePasswordRequirements: PasswordRequirement[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Default password requirements with strong validation rules.
3
+ */
4
+ export const defaultPasswordRequirements = [
5
+ {
6
+ key: "minLength",
7
+ label: "At least 8 characters",
8
+ validate: (password) => password.length >= 8,
9
+ },
10
+ {
11
+ key: "uppercase",
12
+ label: "At least one uppercase letter",
13
+ validate: (password) => /[A-Z]/.test(password),
14
+ },
15
+ {
16
+ key: "lowercase",
17
+ label: "At least one lowercase letter",
18
+ validate: (password) => /[a-z]/.test(password),
19
+ },
20
+ {
21
+ key: "number",
22
+ label: "At least one number",
23
+ validate: (password) => /\d/.test(password),
24
+ },
25
+ {
26
+ key: "special",
27
+ label: "At least one special character",
28
+ validate: (password) => /[!@#$%^&*(),.?":{}|<>]/.test(password),
29
+ },
30
+ ];
31
+ /**
32
+ * Simple password requirements with minimal validation.
33
+ */
34
+ export const simplePasswordRequirements = [
35
+ {
36
+ key: "minLength",
37
+ label: "At least 6 characters",
38
+ validate: (password) => password.length >= 6,
39
+ },
40
+ ];
41
+ //# sourceMappingURL=passwordPresets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordPresets.js","sourceRoot":"","sources":["../../src/signUp/passwordPresets.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAA0B;IAChE;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,uBAAuB;QAC9B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC;KACrD;IACD;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,+BAA+B;QACtC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;KACvD;IACD;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,+BAA+B;QACtC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;KACvD;IACD;QACE,GAAG,EAAE,QAAQ;QACb,KAAK,EAAE,qBAAqB;QAC5B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;KACpD;IACD;QACE,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,gCAAgC;QACvC,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC;KACxE;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAA0B;IAC/D;QACE,GAAG,EAAE,WAAW;QAChB,KAAK,EAAE,uBAAuB;QAC9B,QAAQ,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC;KACrD;CACF,CAAC"}
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Supported OAuth providers for social login buttons.
4
+ */
5
+ export type OAuthProvider = "google" | "github" | "apple";
6
+ /**
7
+ * Configuration for an OAuth provider button.
8
+ */
9
+ export interface OAuthProviderConfig {
10
+ /** The OAuth provider identifier. */
11
+ provider: OAuthProvider;
12
+ /** Callback triggered when the provider button is pressed. */
13
+ onPress: () => Promise<void>;
14
+ /** Whether the button is in a loading state. */
15
+ loading?: boolean;
16
+ /** Whether the button is disabled. */
17
+ disabled?: boolean;
18
+ }
19
+ /**
20
+ * Configuration for a sign-up form field.
21
+ */
22
+ export interface SignUpFieldConfig {
23
+ /** Unique field name used as the key in form state. */
24
+ name: string;
25
+ /** Display label for the field. */
26
+ label: string;
27
+ /** Placeholder text shown when the field is empty. */
28
+ placeholder?: string;
29
+ /** Input type for the field. */
30
+ type?: "text" | "email" | "password";
31
+ /** Whether the field is required. */
32
+ required?: boolean;
33
+ /** Auto-complete hint for the field. */
34
+ autoComplete?: "current-password" | "on" | "off" | "username";
35
+ }
36
+ /**
37
+ * A single password requirement with a label and validation function.
38
+ */
39
+ export interface PasswordRequirement {
40
+ /** Unique key for the requirement. */
41
+ key: string;
42
+ /** Display label for the requirement. */
43
+ label: string;
44
+ /** Returns true if the password meets this requirement. */
45
+ validate: (password: string) => boolean;
46
+ }
47
+ /**
48
+ * Configuration for a single onboarding page in the swiper.
49
+ */
50
+ export interface OnboardingPage {
51
+ /** Title text displayed on the page. */
52
+ title: string;
53
+ /** Subtitle or description text. */
54
+ subtitle?: string;
55
+ /** Custom content to render on the page. */
56
+ content?: ReactNode;
57
+ /** Image source for the page. */
58
+ image?: number | {
59
+ uri: string;
60
+ };
61
+ }
62
+ /**
63
+ * Props for the SignUpScreen component.
64
+ */
65
+ export interface SignUpScreenProps {
66
+ /** Form field configurations. */
67
+ fields: SignUpFieldConfig[];
68
+ /** Callback triggered on form submission. Receives field values as a record. */
69
+ onSubmit: (values: Record<string, string>) => Promise<void>;
70
+ /** Optional OAuth provider configurations for social login buttons. */
71
+ oauthProviders?: OAuthProviderConfig[];
72
+ /** Password requirements to validate against. */
73
+ passwordRequirements?: PasswordRequirement[];
74
+ /** Onboarding pages to display before the sign-up form. */
75
+ onboardingPages?: OnboardingPage[];
76
+ /** Custom logo or banner to display above the form. */
77
+ logo?: ReactNode;
78
+ /** Title text for the sign-up form. */
79
+ title?: string;
80
+ /** Whether the form is in a loading state. */
81
+ loading?: boolean;
82
+ /** Error message to display. */
83
+ error?: string;
84
+ /** Text for the link to navigate to login. */
85
+ loginLinkText?: string;
86
+ /** Callback triggered when the login link is pressed. */
87
+ onLoginPress?: () => void;
88
+ /** Test ID for the root element. */
89
+ testID?: string;
90
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signUpTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signUpTypes.js","sourceRoot":"","sources":["../../src/signUp/signUpTypes.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -41,7 +41,7 @@
41
41
  "expo-web-browser": "~15.0.10",
42
42
  "libphonenumber-js": "^1.12.36",
43
43
  "linkify-it": "^5.0.0",
44
- "lodash": "^4.17.21",
44
+ "lodash": "^4.17.23",
45
45
  "luxon": "^3.7.2",
46
46
  "react-app-polyfill": "^3.0.0",
47
47
  "react-date-picker": "^12.0.1",
@@ -93,6 +93,7 @@
93
93
  "react": "19.1.0",
94
94
  "react-router": "^6.26.2",
95
95
  "react-router-dom": "^6.30.1",
96
+ "react-test-renderer": "19.1.0",
96
97
  "tsc-watch": "^7.1.1",
97
98
  "typescript": "~5.9.2"
98
99
  },
@@ -129,5 +130,5 @@
129
130
  "test:ci": "TZ=America/New_York bun test",
130
131
  "types": "bunx typedoc"
131
132
  },
132
- "version": "0.1.0"
133
+ "version": "0.2.0"
133
134
  }
package/src/Common.ts CHANGED
@@ -1540,6 +1540,58 @@ export interface ButtonProps {
1540
1540
  onClick: () => void | Promise<void>;
1541
1541
  }
1542
1542
 
1543
+ /**
1544
+ * Props for the SocialLoginButton component.
1545
+ * Used for OAuth social login buttons (Google, GitHub, Apple).
1546
+ */
1547
+ export interface SocialLoginButtonProps {
1548
+ /**
1549
+ * The OAuth provider for the social login.
1550
+ */
1551
+ provider: "google" | "github" | "apple";
1552
+
1553
+ /**
1554
+ * The function to call when the button is pressed.
1555
+ * Should initiate the OAuth flow.
1556
+ */
1557
+ onPress: () => Promise<void>;
1558
+
1559
+ /**
1560
+ * If true, a loading spinner will be shown in the button.
1561
+ */
1562
+ loading?: boolean;
1563
+
1564
+ /**
1565
+ * The visual variant of the button.
1566
+ * - "primary": Uses the provider's brand colors
1567
+ * - "outline": Uses an outline style with neutral colors
1568
+ * @default "primary"
1569
+ */
1570
+ variant?: "primary" | "outline";
1571
+
1572
+ /**
1573
+ * If true, the button will be disabled.
1574
+ * @default false
1575
+ */
1576
+ disabled?: boolean;
1577
+
1578
+ /**
1579
+ * If true, the button will take the full width of its container.
1580
+ * @default true
1581
+ */
1582
+ fullWidth?: boolean;
1583
+
1584
+ /**
1585
+ * Custom text for the button. Defaults to "Continue with {Provider}".
1586
+ */
1587
+ text?: string;
1588
+
1589
+ /**
1590
+ * Test ID for testing purposes.
1591
+ */
1592
+ testID?: string;
1593
+ }
1594
+
1543
1595
  export interface CustomSelectFieldProps {
1544
1596
  /**
1545
1597
  * The current value of the custom select field.
@@ -0,0 +1,158 @@
1
+ import {describe, expect, it, mock} from "bun:test";
2
+ import {fireEvent} from "@testing-library/react-native";
3
+
4
+ import {SocialLoginButton} from "./SocialLoginButton";
5
+ import {renderWithTheme} from "./test-utils";
6
+
7
+ describe("SocialLoginButton", () => {
8
+ const createMockOnPress = () => mock(() => Promise.resolve());
9
+
10
+ it("renders correctly with Google provider", () => {
11
+ const onPress = createMockOnPress();
12
+ const {toJSON} = renderWithTheme(<SocialLoginButton onPress={onPress} provider="google" />);
13
+ expect(toJSON()).toMatchSnapshot();
14
+ });
15
+
16
+ it("renders correctly with GitHub provider", () => {
17
+ const onPress = createMockOnPress();
18
+ const {toJSON} = renderWithTheme(<SocialLoginButton onPress={onPress} provider="github" />);
19
+ expect(toJSON()).toMatchSnapshot();
20
+ });
21
+
22
+ it("renders correctly with Apple provider", () => {
23
+ const onPress = createMockOnPress();
24
+ const {toJSON} = renderWithTheme(<SocialLoginButton onPress={onPress} provider="apple" />);
25
+ expect(toJSON()).toMatchSnapshot();
26
+ });
27
+
28
+ it("displays default text for each provider", () => {
29
+ const onPress = createMockOnPress();
30
+
31
+ const {getByText: getGoogleText} = renderWithTheme(
32
+ <SocialLoginButton onPress={onPress} provider="google" />
33
+ );
34
+ expect(getGoogleText("Continue with Google")).toBeTruthy();
35
+
36
+ const {getByText: getGitHubText} = renderWithTheme(
37
+ <SocialLoginButton onPress={onPress} provider="github" />
38
+ );
39
+ expect(getGitHubText("Continue with GitHub")).toBeTruthy();
40
+
41
+ const {getByText: getAppleText} = renderWithTheme(
42
+ <SocialLoginButton onPress={onPress} provider="apple" />
43
+ );
44
+ expect(getAppleText("Continue with Apple")).toBeTruthy();
45
+ });
46
+
47
+ it("displays custom text when provided", () => {
48
+ const onPress = createMockOnPress();
49
+ const customText = "Sign in with Google";
50
+
51
+ const {getByText} = renderWithTheme(
52
+ <SocialLoginButton onPress={onPress} provider="google" text={customText} />
53
+ );
54
+
55
+ expect(getByText(customText)).toBeTruthy();
56
+ });
57
+
58
+ it("renders with outline variant", () => {
59
+ const onPress = createMockOnPress();
60
+ const {toJSON} = renderWithTheme(
61
+ <SocialLoginButton onPress={onPress} provider="google" variant="outline" />
62
+ );
63
+ expect(toJSON()).toMatchSnapshot();
64
+ });
65
+
66
+ it("calls onPress when button is pressed", async () => {
67
+ const onPress = createMockOnPress();
68
+ const {getByTestId} = renderWithTheme(
69
+ <SocialLoginButton onPress={onPress} provider="google" testID="google-login" />
70
+ );
71
+
72
+ const button = getByTestId("google-login");
73
+ fireEvent.press(button);
74
+
75
+ // Wait for debounce
76
+ await new Promise((resolve) => setTimeout(resolve, 600));
77
+
78
+ expect(onPress).toHaveBeenCalled();
79
+ });
80
+
81
+ it("disables button when disabled prop is true", () => {
82
+ const onPress = createMockOnPress();
83
+ const {getByTestId} = renderWithTheme(
84
+ <SocialLoginButton disabled onPress={onPress} provider="google" />
85
+ );
86
+
87
+ const button = getByTestId("social-login-google");
88
+ expect(button.props.disabled).toBe(true);
89
+ });
90
+
91
+ it("disables button when loading is true", () => {
92
+ const onPress = createMockOnPress();
93
+ const {getByTestId} = renderWithTheme(
94
+ <SocialLoginButton loading onPress={onPress} provider="google" />
95
+ );
96
+
97
+ const button = getByTestId("social-login-google");
98
+ expect(button.props.disabled).toBe(true);
99
+ });
100
+
101
+ it("uses correct testID", () => {
102
+ const onPress = createMockOnPress();
103
+ const {getByTestId} = renderWithTheme(
104
+ <SocialLoginButton onPress={onPress} provider="google" testID="custom-test-id" />
105
+ );
106
+
107
+ expect(getByTestId("custom-test-id")).toBeTruthy();
108
+ });
109
+
110
+ it("uses default testID based on provider", () => {
111
+ const onPress = createMockOnPress();
112
+ const {getByTestId} = renderWithTheme(
113
+ <SocialLoginButton onPress={onPress} provider="github" />
114
+ );
115
+
116
+ expect(getByTestId("social-login-github")).toBeTruthy();
117
+ });
118
+
119
+ it("renders full width by default", () => {
120
+ const onPress = createMockOnPress();
121
+ const {getByTestId} = renderWithTheme(
122
+ <SocialLoginButton onPress={onPress} provider="google" />
123
+ );
124
+
125
+ const button = getByTestId("social-login-google");
126
+ expect(button).toHaveStyle({width: "100%"});
127
+ });
128
+
129
+ it("renders auto width when fullWidth is false", () => {
130
+ const onPress = createMockOnPress();
131
+ const {getByTestId} = renderWithTheme(
132
+ <SocialLoginButton fullWidth={false} onPress={onPress} provider="google" />
133
+ );
134
+
135
+ const button = getByTestId("social-login-google");
136
+ expect(button).toHaveStyle({width: "auto"});
137
+ });
138
+
139
+ it("has correct accessibility label", () => {
140
+ const onPress = createMockOnPress();
141
+ const {getByTestId} = renderWithTheme(
142
+ <SocialLoginButton onPress={onPress} provider="google" text="Sign in with Google" />
143
+ );
144
+
145
+ const button = getByTestId("social-login-google");
146
+ expect(button.props["aria-label"]).toBe("Sign in with Google");
147
+ });
148
+
149
+ it("has correct accessibility hint", () => {
150
+ const onPress = createMockOnPress();
151
+ const {getByTestId} = renderWithTheme(
152
+ <SocialLoginButton onPress={onPress} provider="google" />
153
+ );
154
+
155
+ const button = getByTestId("social-login-google");
156
+ expect(button.props.accessibilityHint).toBe("Sign in with Google");
157
+ });
158
+ });
@@ -0,0 +1,182 @@
1
+ import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
2
+ import debounce from "lodash/debounce";
3
+ import {type FC, useMemo, useState} from "react";
4
+ import {ActivityIndicator, Pressable, Text, View} from "react-native";
5
+
6
+ import {Box} from "./Box";
7
+ import type {SocialLoginButtonProps} from "./Common";
8
+ import {useTheme} from "./Theme";
9
+ import {Unifier} from "./Unifier";
10
+
11
+ /**
12
+ * Brand colors for social login providers
13
+ */
14
+ const PROVIDER_COLORS = {
15
+ apple: {
16
+ background: "#000000",
17
+ border: "#000000",
18
+ text: "#ffffff",
19
+ },
20
+ github: {
21
+ background: "#24292e",
22
+ border: "#24292e",
23
+ text: "#ffffff",
24
+ },
25
+ google: {
26
+ background: "#ffffff",
27
+ border: "#dadce0",
28
+ text: "#1f1f1f",
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Font Awesome icon names for social providers
34
+ */
35
+ const PROVIDER_ICONS: Record<string, string> = {
36
+ apple: "apple",
37
+ github: "github",
38
+ google: "google",
39
+ };
40
+
41
+ /**
42
+ * Display names for social providers
43
+ */
44
+ const PROVIDER_NAMES: Record<string, string> = {
45
+ apple: "Apple",
46
+ github: "GitHub",
47
+ google: "Google",
48
+ };
49
+
50
+ /**
51
+ * A branded social login button for OAuth authentication.
52
+ *
53
+ * Supports Google, GitHub, and Apple sign-in with appropriate brand colors
54
+ * and icons following each provider's brand guidelines.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * <SocialLoginButton
59
+ * provider="google"
60
+ * onPress={async () => {
61
+ * await authClient.signIn.social({ provider: "google" });
62
+ * }}
63
+ * />
64
+ * ```
65
+ */
66
+ export const SocialLoginButton: FC<SocialLoginButtonProps> = ({
67
+ provider,
68
+ onPress,
69
+ loading: propsLoading,
70
+ variant = "primary",
71
+ disabled = false,
72
+ fullWidth = true,
73
+ text,
74
+ testID,
75
+ }) => {
76
+ const [loading, setLoading] = useState(propsLoading);
77
+ const {theme} = useTheme();
78
+
79
+ const {backgroundColor, borderColor, textColor} = useMemo(() => {
80
+ const colors = PROVIDER_COLORS[provider];
81
+
82
+ if (variant === "outline") {
83
+ return {
84
+ backgroundColor: theme?.surface.base ?? "#ffffff",
85
+ borderColor: colors.border,
86
+ textColor: theme?.text.primary ?? "#1f1f1f",
87
+ };
88
+ }
89
+
90
+ return {
91
+ backgroundColor: colors.background,
92
+ borderColor: colors.border,
93
+ textColor: colors.text,
94
+ };
95
+ }, [provider, variant, theme]);
96
+
97
+ const iconName = PROVIDER_ICONS[provider];
98
+ const providerName = PROVIDER_NAMES[provider];
99
+ const buttonText = text ?? `Continue with ${providerName}`;
100
+
101
+ const handlePress = useMemo(
102
+ () =>
103
+ debounce(
104
+ async () => {
105
+ await Unifier.utils.haptic();
106
+ setLoading(true);
107
+
108
+ try {
109
+ await onPress();
110
+ } catch (error) {
111
+ console.error(`Social login error (${provider}):`, error);
112
+ } finally {
113
+ setLoading(false);
114
+ }
115
+ },
116
+ 500,
117
+ {leading: true}
118
+ ),
119
+ [onPress, provider]
120
+ );
121
+
122
+ if (!theme) {
123
+ return null;
124
+ }
125
+
126
+ const isDisabled = disabled || loading;
127
+
128
+ return (
129
+ <Pressable
130
+ accessibilityHint={`Sign in with ${providerName}`}
131
+ aria-label={buttonText}
132
+ aria-role="button"
133
+ disabled={isDisabled}
134
+ onPress={handlePress}
135
+ style={{
136
+ alignItems: "center",
137
+ alignSelf: fullWidth ? "stretch" : undefined,
138
+ backgroundColor: isDisabled ? theme.surface.disabled : backgroundColor,
139
+ borderColor,
140
+ borderRadius: theme.radius.rounded,
141
+ borderWidth: 1,
142
+ flexDirection: "row",
143
+ justifyContent: "center",
144
+ opacity: isDisabled ? 0.6 : 1,
145
+ paddingHorizontal: 20,
146
+ paddingVertical: 12,
147
+ width: fullWidth ? "100%" : "auto",
148
+ }}
149
+ testID={testID ?? `social-login-${provider}`}
150
+ >
151
+ <View style={{alignItems: "center", flexDirection: "row"}}>
152
+ {Boolean(iconName) && (
153
+ <View style={{marginRight: 12}}>
154
+ <FontAwesome6
155
+ brand
156
+ color={isDisabled ? theme.text.secondaryLight : textColor}
157
+ name={iconName}
158
+ size={20}
159
+ />
160
+ </View>
161
+ )}
162
+ <Text
163
+ style={{
164
+ color: isDisabled ? theme.text.secondaryLight : textColor,
165
+ fontSize: 16,
166
+ fontWeight: "600",
167
+ }}
168
+ >
169
+ {buttonText}
170
+ </Text>
171
+ {Boolean(loading) && (
172
+ <Box marginLeft={2}>
173
+ <ActivityIndicator
174
+ color={isDisabled ? theme.text.secondaryLight : textColor}
175
+ size="small"
176
+ />
177
+ </Box>
178
+ )}
179
+ </View>
180
+ </Pressable>
181
+ );
182
+ };