@terreno/ui 0.1.0 → 0.3.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/AIRequestExplorer.d.ts +31 -0
- package/dist/AIRequestExplorer.js +44 -0
- package/dist/AIRequestExplorer.js.map +1 -0
- package/dist/AttachmentPreview.d.ts +8 -0
- package/dist/AttachmentPreview.js +16 -0
- package/dist/AttachmentPreview.js.map +1 -0
- package/dist/Common.d.ts +44 -0
- package/dist/FilePickerButton.d.ts +13 -0
- package/dist/FilePickerButton.js +50 -0
- package/dist/FilePickerButton.js.map +1 -0
- package/dist/GPTChat.d.ts +66 -0
- package/dist/GPTChat.js +112 -0
- package/dist/GPTChat.js.map +1 -0
- package/dist/GPTMemoryModal.d.ts +8 -0
- package/dist/GPTMemoryModal.js +14 -0
- package/dist/GPTMemoryModal.js.map +1 -0
- package/dist/SocialLoginButton.d.ts +19 -0
- package/dist/SocialLoginButton.js +119 -0
- package/dist/SocialLoginButton.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -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 +4 -2
- package/src/AIRequestExplorer.tsx +147 -0
- package/src/AttachmentPreview.tsx +63 -0
- package/src/Common.ts +52 -0
- package/src/FilePickerButton.tsx +88 -0
- package/src/GPTChat.tsx +551 -0
- package/src/GPTMemoryModal.tsx +50 -0
- package/src/SocialLoginButton.test.tsx +158 -0
- package/src/SocialLoginButton.tsx +182 -0
- package/src/__snapshots__/SocialLoginButton.test.tsx.snap +277 -0
- package/src/index.tsx +9 -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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import {fireEvent} from "@testing-library/react-native";
|
|
3
|
+
import {renderWithTheme} from "../test-utils";
|
|
4
|
+
import {LoginScreen} from "./LoginScreen";
|
|
5
|
+
|
|
6
|
+
const defaultFields = [
|
|
7
|
+
{label: "Email", name: "email", required: true, type: "email" as const},
|
|
8
|
+
{label: "Password", name: "password", required: true, type: "password" as const},
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe("LoginScreen", () => {
|
|
12
|
+
it("renders with default props", () => {
|
|
13
|
+
const {getByTestId} = renderWithTheme(
|
|
14
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
15
|
+
);
|
|
16
|
+
expect(getByTestId("login-screen")).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("renders title", () => {
|
|
20
|
+
const {getByTestId} = renderWithTheme(
|
|
21
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
22
|
+
);
|
|
23
|
+
expect(getByTestId("login-screen-title")).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders custom title", () => {
|
|
27
|
+
const {getByText} = renderWithTheme(
|
|
28
|
+
<LoginScreen
|
|
29
|
+
fields={defaultFields}
|
|
30
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
31
|
+
title="Sign In"
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
expect(getByText("Sign In")).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders all form fields", () => {
|
|
38
|
+
const {getByTestId} = renderWithTheme(
|
|
39
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
40
|
+
);
|
|
41
|
+
expect(getByTestId("login-screen-email-input")).toBeTruthy();
|
|
42
|
+
expect(getByTestId("login-screen-password-input")).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renders submit button", () => {
|
|
46
|
+
const {getByTestId} = renderWithTheme(
|
|
47
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
48
|
+
);
|
|
49
|
+
expect(getByTestId("login-screen-submit-button")).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders sign-up link when onSignUpPress is provided", () => {
|
|
53
|
+
const {getByTestId} = renderWithTheme(
|
|
54
|
+
<LoginScreen
|
|
55
|
+
fields={defaultFields}
|
|
56
|
+
onSignUpPress={() => {}}
|
|
57
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
expect(getByTestId("login-screen-signup-link")).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders forgot password button when onForgotPasswordPress is provided", () => {
|
|
64
|
+
const {getByTestId} = renderWithTheme(
|
|
65
|
+
<LoginScreen
|
|
66
|
+
fields={defaultFields}
|
|
67
|
+
onForgotPasswordPress={() => {}}
|
|
68
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
expect(getByTestId("login-screen-forgot-password")).toBeTruthy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders error message", () => {
|
|
75
|
+
const {getByTestId} = renderWithTheme(
|
|
76
|
+
<LoginScreen
|
|
77
|
+
error="Invalid credentials"
|
|
78
|
+
fields={defaultFields}
|
|
79
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
expect(getByTestId("login-screen-error")).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("renders OAuth buttons when providers are given", () => {
|
|
86
|
+
const providers = [
|
|
87
|
+
{onPress: mock(() => Promise.resolve()), provider: "google" as const},
|
|
88
|
+
{onPress: mock(() => Promise.resolve()), provider: "github" as const},
|
|
89
|
+
];
|
|
90
|
+
const {getByTestId} = renderWithTheme(
|
|
91
|
+
<LoginScreen
|
|
92
|
+
fields={defaultFields}
|
|
93
|
+
oauthProviders={providers}
|
|
94
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
expect(getByTestId("login-screen-oauth")).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("updates form field values on change", () => {
|
|
101
|
+
const {getByTestId} = renderWithTheme(
|
|
102
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
103
|
+
);
|
|
104
|
+
const emailInput = getByTestId("login-screen-email-input");
|
|
105
|
+
fireEvent.changeText(emailInput, "test@example.com");
|
|
106
|
+
expect(emailInput.props.value).toBe("test@example.com");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders with custom testID", () => {
|
|
110
|
+
const {getByTestId} = renderWithTheme(
|
|
111
|
+
<LoginScreen
|
|
112
|
+
fields={defaultFields}
|
|
113
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
114
|
+
testID="custom-login"
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
expect(getByTestId("custom-login")).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not render forgot password when handler not provided", () => {
|
|
121
|
+
const {queryByTestId} = renderWithTheme(
|
|
122
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
123
|
+
);
|
|
124
|
+
expect(queryByTestId("login-screen-forgot-password")).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("does not render sign-up link when handler not provided", () => {
|
|
128
|
+
const {queryByTestId} = renderWithTheme(
|
|
129
|
+
<LoginScreen fields={defaultFields} onSubmit={mock(() => Promise.resolve())} />
|
|
130
|
+
);
|
|
131
|
+
expect(queryByTestId("login-screen-signup-link")).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders correctly with all props", () => {
|
|
135
|
+
const {toJSON} = renderWithTheme(
|
|
136
|
+
<LoginScreen
|
|
137
|
+
error="Error!"
|
|
138
|
+
fields={defaultFields}
|
|
139
|
+
loading
|
|
140
|
+
onForgotPasswordPress={() => {}}
|
|
141
|
+
onSignUpPress={() => {}}
|
|
142
|
+
onSubmit={mock(() => Promise.resolve())}
|
|
143
|
+
title="Log In"
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
expect(toJSON()).toMatchSnapshot();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {FC} from "react";
|
|
2
|
+
import {useCallback, useState} from "react";
|
|
3
|
+
|
|
4
|
+
import {Box} from "../Box";
|
|
5
|
+
import {Button} from "../Button";
|
|
6
|
+
import {Heading} from "../Heading";
|
|
7
|
+
import {Page} from "../Page";
|
|
8
|
+
import {OAuthButtons} from "../signUp/OAuthButtons";
|
|
9
|
+
import {Text} from "../Text";
|
|
10
|
+
import {TextField} from "../TextField";
|
|
11
|
+
import type {LoginScreenProps} from "./loginTypes";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A configurable login screen component with support for custom fields,
|
|
15
|
+
* OAuth providers, sign-up link, and forgot password link.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <LoginScreen
|
|
20
|
+
* fields={[
|
|
21
|
+
* {name: "email", label: "Email", type: "email", required: true},
|
|
22
|
+
* {name: "password", label: "Password", type: "password", required: true},
|
|
23
|
+
* ]}
|
|
24
|
+
* onSubmit={async (values) => {
|
|
25
|
+
* await signIn(values.email, values.password);
|
|
26
|
+
* }}
|
|
27
|
+
* oauthProviders={[
|
|
28
|
+
* {provider: "google", onPress: () => signInWithSocial("google")},
|
|
29
|
+
* ]}
|
|
30
|
+
* onSignUpPress={() => router.push("/signup")}
|
|
31
|
+
* onForgotPasswordPress={() => router.push("/forgot-password")}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const LoginScreen: FC<LoginScreenProps> = ({
|
|
36
|
+
fields,
|
|
37
|
+
onSubmit,
|
|
38
|
+
oauthProviders,
|
|
39
|
+
logo,
|
|
40
|
+
title = "Welcome Back",
|
|
41
|
+
loading = false,
|
|
42
|
+
error,
|
|
43
|
+
signUpLinkText = "Need an account? Sign Up",
|
|
44
|
+
onSignUpPress,
|
|
45
|
+
forgotPasswordText = "Forgot password?",
|
|
46
|
+
onForgotPasswordPress,
|
|
47
|
+
testID = "login-screen",
|
|
48
|
+
}) => {
|
|
49
|
+
const [formValues, setFormValues] = useState<Record<string, string>>(() => {
|
|
50
|
+
const initial: Record<string, string> = {};
|
|
51
|
+
for (const field of fields) {
|
|
52
|
+
initial[field.name] = "";
|
|
53
|
+
}
|
|
54
|
+
return initial;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handleFieldChange = useCallback((fieldName: string, value: string) => {
|
|
58
|
+
setFormValues((prev) => ({...prev, [fieldName]: value}));
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const handleSubmit = useCallback(async () => {
|
|
62
|
+
await onSubmit(formValues);
|
|
63
|
+
}, [formValues, onSubmit]);
|
|
64
|
+
|
|
65
|
+
const requiredFieldsFilled = fields
|
|
66
|
+
.filter((f) => f.required)
|
|
67
|
+
.every((f) => (formValues[f.name] ?? "").trim().length > 0);
|
|
68
|
+
|
|
69
|
+
const isSubmitDisabled = loading || !requiredFieldsFilled;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Page navigation={undefined}>
|
|
73
|
+
<Box
|
|
74
|
+
alignItems="center"
|
|
75
|
+
alignSelf="center"
|
|
76
|
+
flex="grow"
|
|
77
|
+
justifyContent="center"
|
|
78
|
+
maxWidth={400}
|
|
79
|
+
padding={4}
|
|
80
|
+
testID={testID}
|
|
81
|
+
width="100%"
|
|
82
|
+
>
|
|
83
|
+
{Boolean(logo) && <Box marginBottom={6}>{logo}</Box>}
|
|
84
|
+
|
|
85
|
+
<Box marginBottom={8}>
|
|
86
|
+
<Heading testID={`${testID}-title`}>{title}</Heading>
|
|
87
|
+
</Box>
|
|
88
|
+
|
|
89
|
+
<Box gap={4} width="100%">
|
|
90
|
+
{fields.map((field) => (
|
|
91
|
+
<TextField
|
|
92
|
+
autoComplete={field.autoComplete}
|
|
93
|
+
disabled={loading}
|
|
94
|
+
key={field.name}
|
|
95
|
+
onChange={(value: string) => handleFieldChange(field.name, value)}
|
|
96
|
+
placeholder={field.placeholder ?? field.label}
|
|
97
|
+
testID={`${testID}-${field.name}-input`}
|
|
98
|
+
title={field.label}
|
|
99
|
+
type={
|
|
100
|
+
field.type === "email" ? "email" : field.type === "password" ? "password" : "text"
|
|
101
|
+
}
|
|
102
|
+
value={formValues[field.name]}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
|
|
106
|
+
{Boolean(error) && (
|
|
107
|
+
<Text color="error" testID={`${testID}-error`}>
|
|
108
|
+
{error}
|
|
109
|
+
</Text>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<Box marginTop={4}>
|
|
113
|
+
<Button
|
|
114
|
+
disabled={isSubmitDisabled}
|
|
115
|
+
fullWidth
|
|
116
|
+
loading={loading}
|
|
117
|
+
onClick={handleSubmit}
|
|
118
|
+
testID={`${testID}-submit-button`}
|
|
119
|
+
text="Log In"
|
|
120
|
+
/>
|
|
121
|
+
</Box>
|
|
122
|
+
|
|
123
|
+
{Boolean(onForgotPasswordPress) && (
|
|
124
|
+
<Box alignItems="center" marginTop={2}>
|
|
125
|
+
<Button
|
|
126
|
+
disabled={loading}
|
|
127
|
+
onClick={onForgotPasswordPress!}
|
|
128
|
+
testID={`${testID}-forgot-password`}
|
|
129
|
+
text={forgotPasswordText!}
|
|
130
|
+
variant="muted"
|
|
131
|
+
/>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{Boolean(onSignUpPress) && (
|
|
136
|
+
<Box marginTop={2}>
|
|
137
|
+
<Button
|
|
138
|
+
disabled={loading}
|
|
139
|
+
fullWidth
|
|
140
|
+
onClick={onSignUpPress!}
|
|
141
|
+
testID={`${testID}-signup-link`}
|
|
142
|
+
text={signUpLinkText!}
|
|
143
|
+
variant="outline"
|
|
144
|
+
/>
|
|
145
|
+
</Box>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{Boolean(oauthProviders && oauthProviders.length > 0) && (
|
|
149
|
+
<OAuthButtons
|
|
150
|
+
disabled={loading}
|
|
151
|
+
providers={oauthProviders!}
|
|
152
|
+
testID={`${testID}-oauth`}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
</Box>
|
|
156
|
+
</Box>
|
|
157
|
+
</Page>
|
|
158
|
+
);
|
|
159
|
+
};
|