@terreno/ui 0.0.18 → 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.
- package/dist/Button.js +1 -1
- package/dist/Button.js.map +1 -1
- package/dist/Common.d.ts +45 -1
- package/dist/Hyperlink.js +2 -2
- package/dist/Hyperlink.js.map +1 -1
- package/dist/Page.js +2 -1
- package/dist/Page.js.map +1 -1
- package/dist/SocialLoginButton.d.ts +19 -0
- package/dist/SocialLoginButton.js +119 -0
- package/dist/SocialLoginButton.js.map +1 -0
- package/dist/Text.js +3 -0
- package/dist/Text.js.map +1 -1
- package/dist/Theme.js +27 -27
- package/dist/Theme.js.map +1 -1
- package/dist/Toast.js +5 -2
- package/dist/Toast.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/login/LoginScreen.d.ts +25 -0
- package/dist/login/LoginScreen.js +55 -0
- package/dist/login/LoginScreen.js.map +1 -0
- package/dist/login/index.d.ts +2 -0
- package/dist/login/index.js +2 -0
- package/dist/login/index.js.map +1 -0
- package/dist/login/loginTypes.d.ts +48 -0
- package/dist/login/loginTypes.js +2 -0
- package/dist/login/loginTypes.js.map +1 -0
- package/dist/signUp/OAuthButtons.d.ts +18 -0
- package/dist/signUp/OAuthButtons.js +15 -0
- package/dist/signUp/OAuthButtons.js.map +1 -0
- package/dist/signUp/PasswordRequirements.d.ts +15 -0
- package/dist/signUp/PasswordRequirements.js +14 -0
- package/dist/signUp/PasswordRequirements.js.map +1 -0
- package/dist/signUp/SignUpScreen.d.ts +26 -0
- package/dist/signUp/SignUpScreen.js +64 -0
- package/dist/signUp/SignUpScreen.js.map +1 -0
- package/dist/signUp/Swiper.d.ts +13 -0
- package/dist/signUp/Swiper.js +16 -0
- package/dist/signUp/Swiper.js.map +1 -0
- package/dist/signUp/index.d.ts +6 -0
- package/dist/signUp/index.js +6 -0
- package/dist/signUp/index.js.map +1 -0
- package/dist/signUp/passwordPresets.d.ts +9 -0
- package/dist/signUp/passwordPresets.js +41 -0
- package/dist/signUp/passwordPresets.js.map +1 -0
- package/dist/signUp/signUpTypes.d.ts +90 -0
- package/dist/signUp/signUpTypes.js +2 -0
- package/dist/signUp/signUpTypes.js.map +1 -0
- package/package.json +10 -7
- package/src/Button.tsx +1 -1
- package/src/Common.ts +53 -2
- package/src/Hyperlink.tsx +2 -10
- package/src/Page.tsx +3 -2
- package/src/SocialLoginButton.test.tsx +158 -0
- package/src/SocialLoginButton.tsx +182 -0
- package/src/Text.tsx +3 -0
- package/src/Theme.tsx +33 -27
- package/src/Toast.tsx +5 -2
- package/src/__snapshots__/Button.test.tsx.snap +12 -12
- package/src/__snapshots__/DecimalRangeActionSheet.test.tsx.snap +2 -2
- package/src/__snapshots__/ErrorPage.test.tsx.snap +1 -1
- package/src/__snapshots__/Field.test.tsx.snap +138 -138
- package/src/__snapshots__/HeightActionSheet.test.tsx.snap +2 -2
- package/src/__snapshots__/InfoModalIcon.test.tsx.snap +4 -4
- package/src/__snapshots__/Modal.test.tsx.snap +3 -3
- package/src/__snapshots__/NumberPickerActionSheet.test.tsx.snap +2 -2
- package/src/__snapshots__/Page.test.tsx.snap +1 -1
- package/src/__snapshots__/PhoneNumberField.test.tsx.snap +17 -17
- package/src/__snapshots__/SocialLoginButton.test.tsx.snap +277 -0
- package/src/bunSetup.ts +23 -0
- package/src/index.tsx +6 -0
- package/src/login/LoginScreen.test.tsx +148 -0
- package/src/login/LoginScreen.tsx +159 -0
- package/src/login/__snapshots__/LoginScreen.test.tsx.snap +630 -0
- package/src/login/index.ts +2 -0
- package/src/login/loginTypes.ts +51 -0
- package/src/signUp/OAuthButtons.test.tsx +45 -0
- package/src/signUp/OAuthButtons.tsx +52 -0
- package/src/signUp/PasswordRequirements.test.tsx +41 -0
- package/src/signUp/PasswordRequirements.tsx +49 -0
- package/src/signUp/SignUpScreen.test.tsx +134 -0
- package/src/signUp/SignUpScreen.tsx +172 -0
- package/src/signUp/Swiper.test.tsx +46 -0
- package/src/signUp/Swiper.tsx +59 -0
- package/src/signUp/__snapshots__/OAuthButtons.test.tsx.snap +272 -0
- package/src/signUp/__snapshots__/PasswordRequirements.test.tsx.snap +427 -0
- package/src/signUp/__snapshots__/SignUpScreen.test.tsx.snap +851 -0
- package/src/signUp/__snapshots__/Swiper.test.tsx.snap +249 -0
- package/src/signUp/index.ts +13 -0
- package/src/signUp/passwordPresets.test.ts +57 -0
- package/src/signUp/passwordPresets.ts +43 -0
- package/src/signUp/signUpTypes.ts +94 -0
- package/src/table/__snapshots__/TableDate.test.tsx.snap +4 -4
- package/src/table/__snapshots__/TableRow.test.tsx.snap +64 -64
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.
|
|
@@ -1836,8 +1888,7 @@ export interface NumberPickerActionSheetProps {
|
|
|
1836
1888
|
}
|
|
1837
1889
|
|
|
1838
1890
|
export interface PageProps {
|
|
1839
|
-
|
|
1840
|
-
navigation: any;
|
|
1891
|
+
navigation?: any;
|
|
1841
1892
|
scroll?: boolean;
|
|
1842
1893
|
loading?: boolean;
|
|
1843
1894
|
display?: "flex" | "none" | "block" | "inlineBlock";
|
package/src/Hyperlink.tsx
CHANGED
|
@@ -63,11 +63,7 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
|
63
63
|
const elements = [];
|
|
64
64
|
let _lastIndex = 0;
|
|
65
65
|
|
|
66
|
-
const componentProps =
|
|
67
|
-
...component.props,
|
|
68
|
-
key: undefined,
|
|
69
|
-
ref: undefined,
|
|
70
|
-
};
|
|
66
|
+
const {key: _key, ref: _ref, ...componentProps} = component.props;
|
|
71
67
|
|
|
72
68
|
try {
|
|
73
69
|
linkifyIt.match(component.props.children).forEach(({index, lastIndex, text, url}: any) => {
|
|
@@ -120,11 +116,7 @@ class HyperlinkComponent extends React.Component<HyperlinkProps> {
|
|
|
120
116
|
const {props: {children} = {} as any} = component || {};
|
|
121
117
|
if (!children) return component;
|
|
122
118
|
|
|
123
|
-
const componentProps =
|
|
124
|
-
...component.props,
|
|
125
|
-
key: undefined,
|
|
126
|
-
ref: undefined,
|
|
127
|
-
};
|
|
119
|
+
const {key: _key, ref: _ref, ...componentProps} = component.props;
|
|
128
120
|
|
|
129
121
|
const linkifyIt = this.props.linkify || linkifyLib;
|
|
130
122
|
|
package/src/Page.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {router} from "expo-router";
|
|
1
2
|
import React from "react";
|
|
2
3
|
|
|
3
4
|
import {Box} from "./Box";
|
|
@@ -23,7 +24,7 @@ export class Page extends React.Component<PageProps, {}> {
|
|
|
23
24
|
accessibilityHint="Navigate back"
|
|
24
25
|
accessibilityLabel=""
|
|
25
26
|
iconName="chevron-left"
|
|
26
|
-
onClick={() =>
|
|
27
|
+
onClick={() => router.back()}
|
|
27
28
|
/>
|
|
28
29
|
</Box>
|
|
29
30
|
)}
|
|
@@ -33,7 +34,7 @@ export class Page extends React.Component<PageProps, {}> {
|
|
|
33
34
|
accessibilityHint="Close page"
|
|
34
35
|
accessibilityLabel=""
|
|
35
36
|
iconName="xmark"
|
|
36
|
-
onClick={() =>
|
|
37
|
+
onClick={() => router.back()}
|
|
37
38
|
/>
|
|
38
39
|
</Box>
|
|
39
40
|
)}
|
|
@@ -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
|
+
};
|
package/src/Text.tsx
CHANGED
package/src/Theme.tsx
CHANGED
|
@@ -163,11 +163,39 @@ type DeepPartial<T> = {
|
|
|
163
163
|
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
+
const computeTheme = (
|
|
167
|
+
themeConfig: DeepPartial<TerrenoThemeConfig>,
|
|
168
|
+
primitives: ThemePrimitives
|
|
169
|
+
): TerrenoTheme => {
|
|
170
|
+
const theme = Object.keys(themeConfig).reduce((acc, key) => {
|
|
171
|
+
if (key === "primitives") {
|
|
172
|
+
return acc;
|
|
173
|
+
}
|
|
174
|
+
const value = themeConfig[key as keyof TerrenoThemeConfig] ?? {};
|
|
175
|
+
acc[key as keyof TerrenoTheme] = Object.keys(value).reduce((accKey, valueKey) => {
|
|
176
|
+
const primitiveKey = value[valueKey as keyof typeof value] as keyof ThemePrimitives;
|
|
177
|
+
if (key === "font") {
|
|
178
|
+
accKey[valueKey] = primitiveKey;
|
|
179
|
+
} else {
|
|
180
|
+
if (primitives[primitiveKey] === undefined) {
|
|
181
|
+
console.error(`Primitive ${primitiveKey} not found in theme.`);
|
|
182
|
+
}
|
|
183
|
+
accKey[valueKey as keyof typeof accKey] = primitives[primitiveKey];
|
|
184
|
+
}
|
|
185
|
+
return accKey;
|
|
186
|
+
}, {} as any);
|
|
187
|
+
return acc;
|
|
188
|
+
}, {} as TerrenoTheme);
|
|
189
|
+
return {...theme, primitives};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const defaultComputedTheme = computeTheme(defaultTheme, defaultPrimitives);
|
|
193
|
+
|
|
166
194
|
export const ThemeContext = createContext({
|
|
167
195
|
resetTheme: () => {},
|
|
168
196
|
setPrimitives: (_primitives: DeepPartial<typeof defaultPrimitives>) => {},
|
|
169
197
|
setTheme: (_theme: DeepPartial<TerrenoThemeConfig>) => {},
|
|
170
|
-
theme:
|
|
198
|
+
theme: defaultComputedTheme,
|
|
171
199
|
});
|
|
172
200
|
|
|
173
201
|
interface ThemeProviderProps {
|
|
@@ -178,32 +206,10 @@ export const ThemeProvider = ({children}: ThemeProviderProps) => {
|
|
|
178
206
|
const [providerTheme, setProviderTheme] = useState<DeepPartial<TerrenoThemeConfig>>(defaultTheme);
|
|
179
207
|
const [providerPrimitives, setProviderPrimitives] = useState<ThemePrimitives>(defaultPrimitives);
|
|
180
208
|
|
|
181
|
-
const computedTheme = useMemo(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (key === "primitives") return acc;
|
|
186
|
-
const value = providerTheme[key as keyof TerrenoThemeConfig] ?? {};
|
|
187
|
-
// for each key, map the value to the primitive value.
|
|
188
|
-
acc[key as keyof TerrenoTheme] = Object.keys(value).reduce((accKey, valueKey) => {
|
|
189
|
-
const primitiveKey = value[valueKey as keyof typeof value] as keyof ThemePrimitives;
|
|
190
|
-
if (key === "font") {
|
|
191
|
-
accKey[valueKey] = primitiveKey;
|
|
192
|
-
} else {
|
|
193
|
-
if (providerPrimitives[primitiveKey] === undefined) {
|
|
194
|
-
console.error(`Primitive ${primitiveKey} not found in theme.`);
|
|
195
|
-
}
|
|
196
|
-
accKey[valueKey as keyof typeof accKey] = providerPrimitives[primitiveKey];
|
|
197
|
-
}
|
|
198
|
-
return accKey;
|
|
199
|
-
}, {} as any);
|
|
200
|
-
return acc;
|
|
201
|
-
}, {} as TerrenoTheme);
|
|
202
|
-
return {
|
|
203
|
-
...theme,
|
|
204
|
-
primitives: providerPrimitives,
|
|
205
|
-
};
|
|
206
|
-
}, [providerTheme, providerPrimitives]);
|
|
209
|
+
const computedTheme = useMemo(
|
|
210
|
+
() => computeTheme(providerTheme, providerPrimitives),
|
|
211
|
+
[providerTheme, providerPrimitives]
|
|
212
|
+
);
|
|
207
213
|
|
|
208
214
|
const setPrimitives = (newPrimitives: Partial<ThemePrimitives>) => {
|
|
209
215
|
setProviderPrimitives((prev) => ({...prev, ...newPrimitives}));
|
package/src/Toast.tsx
CHANGED
|
@@ -32,6 +32,10 @@ export function useToast(): {
|
|
|
32
32
|
} {
|
|
33
33
|
const toast = useToastNotifications();
|
|
34
34
|
const show = (title: string, options?: UseToastOptions): string => {
|
|
35
|
+
if (!toast?.show) {
|
|
36
|
+
console.warn("Toast not ready yet — provider ref may not be initialized");
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
35
39
|
const toastData = {
|
|
36
40
|
variant: "info",
|
|
37
41
|
...options,
|
|
@@ -39,7 +43,6 @@ export function useToast(): {
|
|
|
39
43
|
};
|
|
40
44
|
return toast.show(title, {
|
|
41
45
|
data: toastData,
|
|
42
|
-
// a duration of 0 keeps the toast up infinitely until hidden
|
|
43
46
|
duration: options?.persistent ? 0 : TOAST_DURATION_MS,
|
|
44
47
|
});
|
|
45
48
|
};
|
|
@@ -60,7 +63,7 @@ export function useToast(): {
|
|
|
60
63
|
console.error(title);
|
|
61
64
|
return show(title, {...options, variant: "error"});
|
|
62
65
|
},
|
|
63
|
-
hide: (id: string) => toast
|
|
66
|
+
hide: (id: string) => toast?.hide?.(id),
|
|
64
67
|
info: (title: string, options?: UseToastVariantOptions): string => {
|
|
65
68
|
console.info(title);
|
|
66
69
|
return show(title, {...options, variant: "info"});
|
|
@@ -51,7 +51,7 @@ exports[`Button renders correctly with default props 1`] = `
|
|
|
51
51
|
"onPress": [Function: debounced],
|
|
52
52
|
"style": {
|
|
53
53
|
"alignItems": "center",
|
|
54
|
-
"alignSelf":
|
|
54
|
+
"alignSelf": "flex-start",
|
|
55
55
|
"backgroundColor": "#0E9DCD",
|
|
56
56
|
"borderColor": undefined,
|
|
57
57
|
"borderRadius": 360,
|
|
@@ -119,7 +119,7 @@ exports[`Button renders primary variant 1`] = `
|
|
|
119
119
|
"onPress": [Function: debounced],
|
|
120
120
|
"style": {
|
|
121
121
|
"alignItems": "center",
|
|
122
|
-
"alignSelf":
|
|
122
|
+
"alignSelf": "flex-start",
|
|
123
123
|
"backgroundColor": "#0E9DCD",
|
|
124
124
|
"borderColor": undefined,
|
|
125
125
|
"borderRadius": 360,
|
|
@@ -187,7 +187,7 @@ exports[`Button renders secondary variant 1`] = `
|
|
|
187
187
|
"onPress": [Function: debounced],
|
|
188
188
|
"style": {
|
|
189
189
|
"alignItems": "center",
|
|
190
|
-
"alignSelf":
|
|
190
|
+
"alignSelf": "flex-start",
|
|
191
191
|
"backgroundColor": "#2B6072",
|
|
192
192
|
"borderColor": undefined,
|
|
193
193
|
"borderRadius": 360,
|
|
@@ -255,7 +255,7 @@ exports[`Button renders muted variant 1`] = `
|
|
|
255
255
|
"onPress": [Function: debounced],
|
|
256
256
|
"style": {
|
|
257
257
|
"alignItems": "center",
|
|
258
|
-
"alignSelf":
|
|
258
|
+
"alignSelf": "flex-start",
|
|
259
259
|
"backgroundColor": "#B6CDD5",
|
|
260
260
|
"borderColor": undefined,
|
|
261
261
|
"borderRadius": 360,
|
|
@@ -323,7 +323,7 @@ exports[`Button renders outline variant 1`] = `
|
|
|
323
323
|
"onPress": [Function: debounced],
|
|
324
324
|
"style": {
|
|
325
325
|
"alignItems": "center",
|
|
326
|
-
"alignSelf":
|
|
326
|
+
"alignSelf": "flex-start",
|
|
327
327
|
"backgroundColor": "#FFFFFF",
|
|
328
328
|
"borderColor": "#092E3A",
|
|
329
329
|
"borderRadius": 360,
|
|
@@ -391,7 +391,7 @@ exports[`Button renders destructive variant 1`] = `
|
|
|
391
391
|
"onPress": [Function: debounced],
|
|
392
392
|
"style": {
|
|
393
393
|
"alignItems": "center",
|
|
394
|
-
"alignSelf":
|
|
394
|
+
"alignSelf": "flex-start",
|
|
395
395
|
"backgroundColor": "#BD1111",
|
|
396
396
|
"borderColor": undefined,
|
|
397
397
|
"borderRadius": 360,
|
|
@@ -459,7 +459,7 @@ exports[`Button renders disabled state 1`] = `
|
|
|
459
459
|
"onPress": [Function: debounced],
|
|
460
460
|
"style": {
|
|
461
461
|
"alignItems": "center",
|
|
462
|
-
"alignSelf":
|
|
462
|
+
"alignSelf": "flex-start",
|
|
463
463
|
"backgroundColor": "#9A9A9A",
|
|
464
464
|
"borderColor": undefined,
|
|
465
465
|
"borderRadius": 360,
|
|
@@ -527,7 +527,7 @@ exports[`Button applies disabled styles when disabled 1`] = `
|
|
|
527
527
|
"onPress": [Function: debounced],
|
|
528
528
|
"style": {
|
|
529
529
|
"alignItems": "center",
|
|
530
|
-
"alignSelf":
|
|
530
|
+
"alignSelf": "flex-start",
|
|
531
531
|
"backgroundColor": "#9A9A9A",
|
|
532
532
|
"borderColor": undefined,
|
|
533
533
|
"borderRadius": 360,
|
|
@@ -618,7 +618,7 @@ exports[`Button renders loading state 1`] = `
|
|
|
618
618
|
"onPress": [Function: debounced],
|
|
619
619
|
"style": {
|
|
620
620
|
"alignItems": "center",
|
|
621
|
-
"alignSelf":
|
|
621
|
+
"alignSelf": "flex-start",
|
|
622
622
|
"backgroundColor": "#0E9DCD",
|
|
623
623
|
"borderColor": undefined,
|
|
624
624
|
"borderRadius": 360,
|
|
@@ -767,7 +767,7 @@ exports[`Button renders with icon on left 1`] = `
|
|
|
767
767
|
"onPress": [Function: debounced],
|
|
768
768
|
"style": {
|
|
769
769
|
"alignItems": "center",
|
|
770
|
-
"alignSelf":
|
|
770
|
+
"alignSelf": "flex-start",
|
|
771
771
|
"backgroundColor": "#0E9DCD",
|
|
772
772
|
"borderColor": undefined,
|
|
773
773
|
"borderRadius": 360,
|
|
@@ -848,7 +848,7 @@ exports[`Button renders with icon on right 1`] = `
|
|
|
848
848
|
"onPress": [Function: debounced],
|
|
849
849
|
"style": {
|
|
850
850
|
"alignItems": "center",
|
|
851
|
-
"alignSelf":
|
|
851
|
+
"alignSelf": "flex-start",
|
|
852
852
|
"backgroundColor": "#0E9DCD",
|
|
853
853
|
"borderColor": undefined,
|
|
854
854
|
"borderRadius": 360,
|
|
@@ -916,7 +916,7 @@ exports[`Button renders with confirmation modal props 1`] = `
|
|
|
916
916
|
"onPress": [Function: debounced],
|
|
917
917
|
"style": {
|
|
918
918
|
"alignItems": "center",
|
|
919
|
-
"alignSelf":
|
|
919
|
+
"alignSelf": "flex-start",
|
|
920
920
|
"backgroundColor": "#0E9DCD",
|
|
921
921
|
"borderColor": undefined,
|
|
922
922
|
"borderRadius": 360,
|