mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
mobile: update ui for auth screens
This commit is contained in:
committed by
Abdullah Atta
parent
d9127df1a3
commit
e9711fd385
@@ -24,6 +24,9 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
import { AuthParams } from "../../stores/use-navigation-store";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { FontSizes } from "../../common/design/font";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const route = useRoute();
|
||||
@@ -37,19 +40,25 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
{props.welcome ? (
|
||||
<ProgressPills activePillIndex={1} />
|
||||
) : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth((route.params as AuthParams)?.context);
|
||||
}}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30
|
||||
}}
|
||||
size={26}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
@@ -60,16 +69,16 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
iconSize={24}
|
||||
fontSize={FontSizes.SM}
|
||||
type="plain"
|
||||
fontFamily="REGULAR"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
paddingRight: 0,
|
||||
paddingHorizontal: 0,
|
||||
paddingVertical: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -21,8 +21,10 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { RouteProp, useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
@@ -33,24 +35,23 @@ import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { AuthHeader } from "./header";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { useLogin } from "./use-login";
|
||||
import FormInput, { validators } from "../ui/input/form-input";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
mfaAuth: 2,
|
||||
passwordAuth: 3
|
||||
mfaAuth: 2
|
||||
};
|
||||
|
||||
export const Login = ({
|
||||
@@ -68,8 +69,9 @@ export const Login = ({
|
||||
passwordInputRef,
|
||||
loading,
|
||||
setLoading,
|
||||
login,
|
||||
error,
|
||||
login,
|
||||
mfaData,
|
||||
formRef
|
||||
} = useLogin(async () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
@@ -91,8 +93,6 @@ export const Login = ({
|
||||
Progress.present();
|
||||
}
|
||||
});
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
|
||||
const onContinue = () => {
|
||||
login();
|
||||
@@ -114,249 +114,249 @@ export const Login = ({
|
||||
return (
|
||||
<>
|
||||
<AuthHeader />
|
||||
<Dialog context="two_factor_verify" />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
marginTop: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<ProgressPills
|
||||
count={2}
|
||||
activePillIndex={step === LoginSteps.emailAuth ? 0 : 1}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
minHeight: "99%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
{step === LoginSteps.emailAuth ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.4
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
extraBold
|
||||
size={AppFontSize.xxl}
|
||||
fontSize="XL"
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: DDS.isTab ? 0 : DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
onContinue();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
onContinue();
|
||||
>
|
||||
<FormInput
|
||||
name="email"
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
marginBottom={0}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
validators.email(strings.enterAValidEmailAddress())
|
||||
]}
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<FormInput
|
||||
name="password"
|
||||
formRef={formRef}
|
||||
fwdRef={passwordInputRef}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
onContinue();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading || !formRef.current.getValue("email")) return;
|
||||
presentSheet({
|
||||
component: (
|
||||
<ForgotPassword
|
||||
userEmail={formRef.current.getValue("email")}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}}
|
||||
fontFamily="REGULAR"
|
||||
fontSize={AppFontSize.sm}
|
||||
type="plain"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_1,
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
type="secondaryAccented"
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
fontSize={AppFontSize.sm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingVertical: 0
|
||||
}}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.signUp()}
|
||||
<Paragraph fontSize="SM" color={colors.secondary.paragraph}>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
fontSize="SM"
|
||||
style={{ color: colors.primary.accent }}
|
||||
fontFamily="SEMI_BOLD"
|
||||
>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
{error ? (
|
||||
<Paragraph
|
||||
numberOfLines={4}
|
||||
onPress={() => {}}
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<AppIcon
|
||||
color={colors.error.accent}
|
||||
name="alert-circle-outline"
|
||||
size={AppFontSize.sm - 1}
|
||||
/>{" "}
|
||||
{error.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<TwoFactorVerification
|
||||
onMfaLogin={async (
|
||||
mfa: any,
|
||||
callback: (success: boolean) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
await login();
|
||||
|
||||
if (success) {
|
||||
setLoading(false);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
}}
|
||||
mfaInfo={
|
||||
mfaData.current || {
|
||||
primaryMethod: "email",
|
||||
secondaryMethod: "sms",
|
||||
token: ""
|
||||
}
|
||||
}
|
||||
onCancel={() => {
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,10 @@ import { SignupContext } from "./signup-context";
|
||||
import { RouteParams } from "../../stores/use-navigation-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Spacing } from "../../common/design/spacing";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import { SETTING_ACCOUNT_SVG } from "../../assets/images/assets";
|
||||
import { ProgressPills } from "../intro/progress-pills";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
@@ -133,7 +137,7 @@ export const Signup = ({
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
minHeight: "99%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
keyboardShouldPersistTaps="handled"
|
||||
@@ -145,68 +149,24 @@ export const Signup = ({
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
height: "100%",
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
paddingTop: Spacing.LEVEL_6
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<Heading
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: isTablet ? "center" : undefined,
|
||||
borderWidth: isTablet ? 1 : undefined,
|
||||
borderColor: isTablet ? colors.primary.border : undefined,
|
||||
borderRadius: isTablet ? 20 : undefined,
|
||||
marginTop: isTablet ? 50 : undefined,
|
||||
width: !isTablet ? undefined : "50%",
|
||||
minHeight: height * 0.25
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
fontSize="XL"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={AppFontSize.xxl}
|
||||
>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
{strings.createAccount()}
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: DDS.isTab ? 0 : 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center"
|
||||
gap: Spacing.LEVEL_2,
|
||||
paddingBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
@@ -214,6 +174,7 @@ export const Signup = ({
|
||||
formRef={formRef}
|
||||
fwdRef={emailInputRef}
|
||||
loading={loading}
|
||||
label={strings.email()}
|
||||
testID="input.email"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
@@ -222,6 +183,7 @@ export const Signup = ({
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
placeholder={strings.email()}
|
||||
placeholder="you@example.com"
|
||||
blurOnSubmit={false}
|
||||
validators={[
|
||||
validators.required(strings.emailRequired()),
|
||||
@@ -245,9 +207,11 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
label={strings.password()}
|
||||
placeholder="•••••••••"
|
||||
validators={[validators.required(strings.passwordRequired())]}
|
||||
onSubmitEditing={() => {
|
||||
if (formRef.current.validateField("password")) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
@@ -265,8 +229,8 @@ export const Signup = ({
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
label={strings.confirmPassword()}
|
||||
placeholder="•••••••••"
|
||||
validators={[
|
||||
validators.required(strings.confirmPasswordRequired()),
|
||||
validators.matchField(
|
||||
@@ -278,7 +242,17 @@ export const Signup = ({
|
||||
signup();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1,
|
||||
alignSelf: "center",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
@@ -296,18 +270,14 @@ export const Signup = ({
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
<Paragraph fontSize="SM" color={colors.primary.paragraph}>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={AppFontSize.xs + 1}
|
||||
fontSize="SM"
|
||||
fontFamily="SEMI_BOLD"
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
{strings.login()}
|
||||
@@ -347,12 +317,12 @@ export const Signup = ({
|
||||
marginBottom: 25,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={AppFontSize.xxs}
|
||||
color={colors.secondary.paragraph}
|
||||
fontSize="XS"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
@@ -366,7 +336,7 @@ export const Signup = ({
|
||||
</Paragraph>{" "}
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={AppFontSize.xxs}
|
||||
fontSize="XS"
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
@@ -385,12 +355,22 @@ export const Signup = ({
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: Spacing.LEVEL_3
|
||||
}}
|
||||
>
|
||||
<ProgressPills activePillIndex={2} />
|
||||
<Loading
|
||||
title={"Setting up your account..."}
|
||||
svgSrc={SETTING_ACCOUNT_SVG}
|
||||
description="Your account is almost ready, please wait..."
|
||||
style={{
|
||||
height: undefined,
|
||||
marginTop: Spacing.LEVEL_6
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,6 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database/index";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
@@ -32,10 +31,11 @@ import { presentDialog } from "../dialog/functions";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import PinInput from "../ui/pin-input/index";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
type MFAInfo = {
|
||||
primaryMethod: string;
|
||||
@@ -73,6 +73,10 @@ const TwoFactorVerification = ({
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [codeInput, setCodeInput] = useState("");
|
||||
|
||||
const isRecoveryCode = currentMethod.method === "recoveryCode";
|
||||
const codeLength = isRecoveryCode ? 10 : 6;
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) {
|
||||
@@ -94,10 +98,7 @@ const TwoFactorVerification = ({
|
||||
method: currentMethod.method,
|
||||
code: code.current
|
||||
},
|
||||
(result) => {
|
||||
if (result) {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
}
|
||||
() => {
|
||||
setLoading(false);
|
||||
},
|
||||
(e) => {
|
||||
@@ -168,112 +169,86 @@ const TwoFactorVerification = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentMethod.method]);
|
||||
|
||||
useEffect(() => {
|
||||
setCodeInput("");
|
||||
code.current = "";
|
||||
}, [currentMethod.method]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 60
|
||||
}}
|
||||
<View
|
||||
onLayout={() => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 500);
|
||||
}}
|
||||
style={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<IconButton
|
||||
style={{
|
||||
alignItems: "center",
|
||||
paddingHorizontal: currentMethod.method ? 12 : 0,
|
||||
gap: 12
|
||||
width: 50,
|
||||
height: 50,
|
||||
backgroundColor: colors.primary.shade,
|
||||
borderRadius: Radius.XS,
|
||||
marginBottom: Spacing.LEVEL_7
|
||||
}}
|
||||
size={25}
|
||||
name="key"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
width: 70,
|
||||
height: 70
|
||||
}}
|
||||
size={50}
|
||||
name="key"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
width: "80%",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{currentMethod.method
|
||||
? strings["2faCodeHelpText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeHelpText"]
|
||||
]?.() || strings.select2faCodeHelpText()
|
||||
: strings.select2faCodeHelpText()}
|
||||
</Paragraph>
|
||||
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
width: "80%",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{currentMethod.method
|
||||
? strings["2faCodeHelpText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeHelpText"]
|
||||
]?.() || strings.select2faCodeHelpText()
|
||||
: strings.select2faCodeHelpText()}
|
||||
</Paragraph>
|
||||
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "plain" : "transparent"}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder={
|
||||
currentMethod.method === "recoveryCode"
|
||||
? "xxxxx-xxxxx"
|
||||
: "xxxxxx"
|
||||
}
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
marginTop: Spacing.LEVEL_4,
|
||||
borderRadius: Radius.S,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
paddingVertical: Spacing.LEVEL_4,
|
||||
paddingHorizontal: Spacing.LEVEL_3,
|
||||
width: "100%",
|
||||
gap: Spacing.LEVEL_4
|
||||
}}
|
||||
>
|
||||
<PinInput
|
||||
testID={"input.totp"}
|
||||
maxLength={
|
||||
currentMethod.method === "recoveryCode" ? undefined : 6
|
||||
}
|
||||
fwdRef={inputRef}
|
||||
textAlign="center"
|
||||
onChangeText={(value) => {
|
||||
setError(undefined);
|
||||
inputRef={inputRef}
|
||||
length={codeLength}
|
||||
value={codeInput}
|
||||
onChangeText={(value: string) => {
|
||||
setCodeInput(value);
|
||||
code.current = value;
|
||||
}}
|
||||
cursorColor={colors.selected.accent}
|
||||
selectionHandleColor={colors.selected.accent}
|
||||
selectionColor={colors.selected.accent}
|
||||
onSubmitEditing={onNext}
|
||||
height={60}
|
||||
marginBottom={0}
|
||||
inputStyle={{
|
||||
fontSize: AppFontSize.lg,
|
||||
textAlign: "center",
|
||||
letterSpacing: 7,
|
||||
width: 250
|
||||
}}
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
minWidth: "50%"
|
||||
keyboardType={isRecoveryCode ? "default" : "number-pad"}
|
||||
sanitize={(value: string) => {
|
||||
if (isRecoveryCode) {
|
||||
return value.replace(/[^0-9a-zA-Z]/g, "").toUpperCase();
|
||||
}
|
||||
|
||||
return value.replace(/[^0-9]/g, "");
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
@@ -296,76 +271,112 @@ const TwoFactorVerification = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
title={loading ? null : strings.next()}
|
||||
type="accent"
|
||||
width={250}
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
/>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
reset();
|
||||
onCancel();
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
width={250}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
>
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
reset();
|
||||
onCancel();
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 0,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start"
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={loading ? null : strings.continue()}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
style={{
|
||||
width: "48%"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{currentMethod.method === "sms" ||
|
||||
currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
type={seconds ? "secondary" : "transparent"}
|
||||
disabled={!seconds}
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(`${seconds}`)
|
||||
: strings.resendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
fontSize={AppFontSize.sm}
|
||||
style={{
|
||||
paddingVertical: 0,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method as keyof (typeof strings)["2faCodeSecondaryMethodText"]
|
||||
]()}
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{getMethods().map((item) => (
|
||||
<Pressable
|
||||
key={item.title}
|
||||
onPress={() => {
|
||||
setCurrentMethod({
|
||||
method: item.id,
|
||||
isPrimary: false
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: 0,
|
||||
flexDirection: "row",
|
||||
borderRadius: 0,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
color={colors.primary.accent}
|
||||
name={item.icon}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="secondaryAccented"
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
size={15}
|
||||
color={colors.primary.accent}
|
||||
name={item.icon}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Paragraph size={AppFontSize.md}>{item.title}</Paragraph>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,19 +21,16 @@ import { strings } from "@notesnook/intl";
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSimpleDialog } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { createFormRef } from "../ui/input/form-input";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
mfaAuth: 2,
|
||||
passwordAuth: 3
|
||||
mfaAuth: 2
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
@@ -46,6 +43,7 @@ export const useLogin = (
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const mfaData = useRef<any>(undefined);
|
||||
const formRef = useRef(
|
||||
createFormRef({
|
||||
email: "",
|
||||
@@ -67,50 +65,16 @@ export const useLogin = (
|
||||
const mfaInfo = await db.user.authenticateEmail(
|
||||
formRef.current.getValue("email")
|
||||
);
|
||||
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (mfa: any, callback: (success: boolean) => void, onerror: (e: Error) => void) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 500);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
ToastManager.error(new Error("Token expired, try logging in again"));
|
||||
} else {
|
||||
onerror(e as Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
mfaInfo,
|
||||
() => {
|
||||
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
);
|
||||
mfaData.current = mfaInfo;
|
||||
setStep(LoginSteps.mfaAuth);
|
||||
setLoading(false);
|
||||
} else {
|
||||
finishWithError(new Error(strings.unableToSend2faCode()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
case LoginSteps.mfaAuth: {
|
||||
if (!formRef.current.validate()) {
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -172,6 +136,7 @@ export const useLogin = (
|
||||
setLoading,
|
||||
error,
|
||||
setError,
|
||||
formRef
|
||||
formRef,
|
||||
mfaData
|
||||
};
|
||||
};
|
||||
|
||||
@@ -56,17 +56,26 @@ const Intro = () => {
|
||||
gap: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{item.headings?.map((heading) =>
|
||||
{item.headings?.map((heading, index) =>
|
||||
heading.bold ? (
|
||||
<Heading
|
||||
key={heading.value()}
|
||||
fontFamily="SEMI_BOLD"
|
||||
fontSize="XXL"
|
||||
style={{
|
||||
marginTop: index !== 0 ? -5 : undefined
|
||||
}}
|
||||
>
|
||||
{heading.value()}
|
||||
</Heading>
|
||||
) : (
|
||||
<Paragraph fontFamily="MEDIUM" fontSize="XL">
|
||||
<Paragraph
|
||||
style={{
|
||||
marginTop: index !== 0 ? -5 : undefined
|
||||
}}
|
||||
fontFamily="MEDIUM"
|
||||
fontSize="XL"
|
||||
>
|
||||
{heading.value()}
|
||||
</Paragraph>
|
||||
)
|
||||
|
||||
@@ -22,7 +22,10 @@ import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
|
||||
export const ProgressPills = (props: { activePillIndex: number }) => {
|
||||
export const ProgressPills = (props: {
|
||||
activePillIndex: number;
|
||||
count?: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
@@ -31,41 +34,19 @@ export const ProgressPills = (props: { activePillIndex: number }) => {
|
||||
gap: Spacing.LEVEL_1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: props.activePillIndex === 0 ? 26 : 14,
|
||||
height: 5,
|
||||
backgroundColor:
|
||||
props.activePillIndex === 0
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: props.activePillIndex === 1 ? 26 : 14,
|
||||
height: 5,
|
||||
backgroundColor:
|
||||
props.activePillIndex === 1
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: Radius.XXS
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: props.activePillIndex === 2 ? 26 : 14,
|
||||
height: 5,
|
||||
backgroundColor:
|
||||
props.activePillIndex === 2
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: Radius.XXS
|
||||
}}
|
||||
/>
|
||||
{new Array(props.count || 3).fill(0).map((item, index) => (
|
||||
<View
|
||||
style={{
|
||||
width: props.activePillIndex === index ? 26 : 14,
|
||||
height: 5,
|
||||
backgroundColor:
|
||||
props.activePillIndex === index
|
||||
? colors.primary.accent
|
||||
: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,15 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { View, ViewStyle } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import { Radius, Spacing } from "../../common/design/spacing";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
svgSrc?: string;
|
||||
style?: ViewStyle;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
@@ -37,18 +41,37 @@ export const Loading = (props: {
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
paddingHorizontal: 16,
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.svgSrc ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_7
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
src={props.svgSrc}
|
||||
style={{
|
||||
width: 180,
|
||||
height: 180
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
fontSize="XL"
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
@@ -57,8 +80,10 @@ export const Loading = (props: {
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
marginBottom: Spacing.LEVEL_4
|
||||
}}
|
||||
fontSize="SM"
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
@@ -67,15 +92,15 @@ export const Loading = (props: {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
width: 300
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
height={7}
|
||||
width={300}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
borderRadius={Radius.XS}
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
|
||||
@@ -33,6 +33,8 @@ import NativeTooltip from "../../../utils/tooltip";
|
||||
import { Pressable, PressableProps, useButton } from "../pressable";
|
||||
import Heading from "../typography/heading";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
import { FontFamily } from "../../../common/design/font";
|
||||
export interface ButtonProps extends PressableProps {
|
||||
height?: number;
|
||||
icon?: string;
|
||||
@@ -44,6 +46,7 @@ export interface ButtonProps extends PressableProps {
|
||||
title?: string | null;
|
||||
loading?: boolean;
|
||||
width?: string | number | null;
|
||||
fontFamily?: keyof typeof FontFamily;
|
||||
buttonType?: {
|
||||
text?: ColorValue;
|
||||
selected?: ColorValue;
|
||||
@@ -64,6 +67,7 @@ export const Button = ({
|
||||
loading = false,
|
||||
title = null,
|
||||
icon,
|
||||
fontFamily = "SEMI_BOLD",
|
||||
fontSize = AppFontSize.md,
|
||||
type = "transparent",
|
||||
iconSize = AppFontSize.md,
|
||||
@@ -152,16 +156,17 @@ export const Button = ({
|
||||
color={textColor as string}
|
||||
size={fontSize}
|
||||
numberOfLines={1}
|
||||
fontFamily={fontFamily}
|
||||
allowFontScaling={allowFontScaling}
|
||||
style={[
|
||||
{
|
||||
marginLeft:
|
||||
icon || (loading && iconPosition === "left")
|
||||
? DefaultAppStyles.GAP_SMALL
|
||||
? Spacing.LEVEL_1
|
||||
: 0,
|
||||
marginRight:
|
||||
icon || (loading && iconPosition === "right")
|
||||
? DefaultAppStyles.GAP_SMALL
|
||||
? Spacing.LEVEL_1
|
||||
: 0
|
||||
},
|
||||
textStyle
|
||||
@@ -175,7 +180,7 @@ export const Button = ({
|
||||
<Icon
|
||||
name={icon}
|
||||
allowFontScaling
|
||||
style={[{ marginLeft: 0 }, iconStyle as any]}
|
||||
style={[iconStyle as any]}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
/>
|
||||
|
||||
@@ -43,6 +43,7 @@ import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
import isURL from "validator/lib/isURL";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { Spacing } from "../../../common/design/spacing";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput | null>;
|
||||
@@ -80,6 +81,7 @@ interface InputProps extends TextInputProps {
|
||||
containerStyle?: ViewStyle;
|
||||
wrapperStyle?: ViewStyle;
|
||||
flexGrow?: number;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const Input = ({
|
||||
@@ -93,7 +95,7 @@ const Input = ({
|
||||
secureTextEntry,
|
||||
customColor,
|
||||
customValidator,
|
||||
marginBottom = 10,
|
||||
marginBottom = 0,
|
||||
button,
|
||||
onBlurInput,
|
||||
onPress,
|
||||
@@ -103,6 +105,7 @@ const Input = ({
|
||||
buttons,
|
||||
marginRight,
|
||||
buttonLeft,
|
||||
label,
|
||||
flexGrow = 1,
|
||||
inputStyle = {},
|
||||
containerStyle = {},
|
||||
@@ -222,11 +225,11 @@ const Input = ({
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingHorizontal: Spacing.LEVEL_2,
|
||||
paddingRight:
|
||||
buttons || button || secureTextEntry || error
|
||||
? DefaultAppStyles.GAP
|
||||
: DefaultAppStyles.GAP,
|
||||
? Spacing.LEVEL_3
|
||||
: Spacing.LEVEL_3,
|
||||
...containerStyle
|
||||
};
|
||||
|
||||
@@ -235,10 +238,10 @@ const Input = ({
|
||||
fontSize: fontSize,
|
||||
color:
|
||||
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
|
||||
paddingTop: Spacing.LEVEL_3,
|
||||
paddingBottom: Spacing.LEVEL_3,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
@@ -253,6 +256,17 @@ const Input = ({
|
||||
...wrapperStyle
|
||||
}}
|
||||
>
|
||||
{label ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: Spacing.LEVEL_1
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
fontSize="XS"
|
||||
>
|
||||
{label}
|
||||
</Paragraph>
|
||||
) : undefined}
|
||||
<TouchableOpacity
|
||||
disabled={!loading}
|
||||
onPress={onPress}
|
||||
@@ -264,6 +278,9 @@ const Input = ({
|
||||
<TextInput
|
||||
{...restProps}
|
||||
ref={fwdRef}
|
||||
onLayout={(e) => {
|
||||
console.log(e.nativeEvent.layout);
|
||||
}}
|
||||
editable={!loading && restProps.editable}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlur}
|
||||
@@ -376,37 +393,36 @@ const Input = ({
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{validationType === "password" && focus && (
|
||||
{validationType === "password" &&
|
||||
focus &&
|
||||
Object.keys(errorList).filter((k) => errorList[k as ErrorKey] === true)
|
||||
.length !== 0 ? (
|
||||
<View
|
||||
style={{
|
||||
marginTop: -5,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
{Object.keys(errorList).filter(
|
||||
(k) => errorList[k as ErrorKey] === true
|
||||
).length !== 0
|
||||
? Object.keys(ERRORS_LIST).map((error) => (
|
||||
<View
|
||||
key={ERRORS_LIST[error as ErrorKey]}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={errorList[error as ErrorKey] ? "close" : "check"}
|
||||
color={errorList[error as ErrorKey] ? "red" : "green"}
|
||||
/>
|
||||
{Object.keys(ERRORS_LIST).map((error) => (
|
||||
<View
|
||||
key={ERRORS_LIST[error as ErrorKey]}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={errorList[error as ErrorKey] ? "close" : "check"}
|
||||
color={errorList[error as ErrorKey] ? "red" : "green"}
|
||||
/>
|
||||
|
||||
<Paragraph style={{ marginLeft: 5 }} size={AppFontSize.xs}>
|
||||
{ERRORS_LIST[error as ErrorKey]}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))
|
||||
: null}
|
||||
<Paragraph style={{ marginLeft: 5 }} fontSize="XS">
|
||||
{ERRORS_LIST[error as ErrorKey]}
|
||||
</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
340
apps/mobile/app/components/ui/pin-input/index.tsx
Normal file
340
apps/mobile/app/components/ui/pin-input/index.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppState,
|
||||
AppStateStatus,
|
||||
KeyboardTypeOptions,
|
||||
TextInput,
|
||||
TextInputKeyPressEvent,
|
||||
TextInputProps,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Radius, Spacing } from "../../../common/design/spacing";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
|
||||
type PinInputProps = {
|
||||
value: string;
|
||||
length: number;
|
||||
testID?: string;
|
||||
autoFocus?: boolean;
|
||||
inputRef?: RefObject<TextInput | null>;
|
||||
keyboardType?: KeyboardTypeOptions;
|
||||
onSubmitEditing?: TextInputProps["onSubmitEditing"];
|
||||
onChangeText: (value: string) => void;
|
||||
sanitize?: (value: string) => string;
|
||||
};
|
||||
|
||||
const defaultSanitizer = (value: string) => value;
|
||||
|
||||
const PinInput = ({
|
||||
value,
|
||||
length,
|
||||
testID,
|
||||
autoFocus,
|
||||
inputRef,
|
||||
keyboardType = "number-pad",
|
||||
onSubmitEditing,
|
||||
onChangeText,
|
||||
sanitize = defaultSanitizer
|
||||
}: PinInputProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const inputRefs = useRef<Array<TextInput | null>>([]);
|
||||
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
|
||||
const lastAutoFilledValue = useRef<string>("");
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
|
||||
const normalizeToCells = useMemo(
|
||||
() => (raw: string) => {
|
||||
const sanitized = sanitize(raw).slice(0, length);
|
||||
const next = Array.from({ length }, () => "");
|
||||
sanitized.split("").forEach((char, index) => {
|
||||
next[index] = char;
|
||||
});
|
||||
return next;
|
||||
},
|
||||
[length, sanitize]
|
||||
);
|
||||
|
||||
const [cells, setCells] = useState<string[]>(() => normalizeToCells(value));
|
||||
|
||||
const emitChange = React.useCallback(
|
||||
(nextCells: string[]) => {
|
||||
onChangeText(nextCells.join(""));
|
||||
},
|
||||
[onChangeText]
|
||||
);
|
||||
|
||||
const focusCell = React.useCallback(
|
||||
(index: number) => {
|
||||
const clampedIndex = Math.max(0, Math.min(index, length - 1));
|
||||
requestAnimationFrame(() => {
|
||||
inputRefs.current[clampedIndex]?.focus();
|
||||
});
|
||||
},
|
||||
[length]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus) {
|
||||
focusCell(0);
|
||||
}
|
||||
}, [autoFocus, focusCell]);
|
||||
|
||||
useEffect(() => {
|
||||
// Parent-driven resets should clear all slots.
|
||||
if (!value) {
|
||||
setCells(Array.from({ length }, () => ""));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent-driven complete values (OTP autofill, test paste) should sync.
|
||||
if (value.length >= length) {
|
||||
setCells(normalizeToCells(value));
|
||||
}
|
||||
}, [length, normalizeToCells, value]);
|
||||
|
||||
const onCellChange = (index: number, text: string) => {
|
||||
const sanitized = sanitize(text);
|
||||
const nextCells = [...cells];
|
||||
|
||||
if (!sanitized) {
|
||||
nextCells[index] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = sanitized.split("");
|
||||
let cursor = index;
|
||||
chars.forEach((char) => {
|
||||
if (cursor < length) {
|
||||
nextCells[cursor] = char;
|
||||
cursor += 1;
|
||||
}
|
||||
});
|
||||
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
|
||||
if (cursor < length) {
|
||||
focusCell(cursor);
|
||||
} else {
|
||||
inputRefs.current[length - 1]?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const onCellKeyPress = (index: number, event: TextInputKeyPressEvent) => {
|
||||
if (event.nativeEvent.key !== "Backspace") return;
|
||||
|
||||
const nextCells = [...cells];
|
||||
|
||||
if (nextCells[index]) {
|
||||
nextCells[index] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
nextCells[index - 1] = "";
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
focusCell(index - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const onBulkInputChange = (text: string) => {
|
||||
const sanitized = sanitize(text).slice(0, length);
|
||||
const nextCells = normalizeToCells(sanitized);
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
|
||||
if (sanitized.length < length) {
|
||||
focusCell(sanitized.length);
|
||||
}
|
||||
};
|
||||
|
||||
const applyClipboardCode = React.useCallback(
|
||||
(clipboardText: string) => {
|
||||
const sanitized = sanitize(clipboardText).slice(0, length);
|
||||
if (sanitized.length !== length) return;
|
||||
|
||||
const currentJoined = cells.join("");
|
||||
if (
|
||||
currentJoined === sanitized ||
|
||||
lastAutoFilledValue.current === sanitized
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextCells = normalizeToCells(sanitized);
|
||||
setCells(nextCells);
|
||||
emitChange(nextCells);
|
||||
lastAutoFilledValue.current = sanitized;
|
||||
inputRefs.current[length - 1]?.blur();
|
||||
},
|
||||
[cells, emitChange, length, normalizeToCells, sanitize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const readClipboardAndApply = async () => {
|
||||
try {
|
||||
const clipboardText = await Clipboard.getString();
|
||||
if (!clipboardText) return;
|
||||
|
||||
// Prefer not to override a fully entered code while user is interacting.
|
||||
if (cells.join("").length >= length) return;
|
||||
|
||||
applyClipboardCode(clipboardText);
|
||||
} catch {
|
||||
// Ignore clipboard access failures.
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener("change", (nextState) => {
|
||||
const wasInBackground =
|
||||
appStateRef.current === "background" ||
|
||||
appStateRef.current === "inactive";
|
||||
|
||||
if (wasInBackground && nextState === "active") {
|
||||
void readClipboardAndApply();
|
||||
}
|
||||
|
||||
appStateRef.current = nextState;
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
}, [cells, length, normalizeToCells, sanitize, applyClipboardCode]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexWrap: "nowrap",
|
||||
rowGap: Spacing.LEVEL_1,
|
||||
columnGap: Spacing.LEVEL_1,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: Radius.S,
|
||||
paddingVertical: Spacing.LEVEL_3,
|
||||
paddingHorizontal: Spacing.LEVEL_2
|
||||
}}
|
||||
>
|
||||
{cells.map((char, index) => {
|
||||
const isActive = focusedIndex === index;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`${index}`}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => focusCell(index)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: isActive
|
||||
? colors.selected.accent
|
||||
: colors.primary.shade,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={(ref) => {
|
||||
inputRefs.current[index] = ref;
|
||||
if (index === 0 && inputRef) {
|
||||
inputRef.current = ref;
|
||||
}
|
||||
}}
|
||||
disableFullscreenUI={true}
|
||||
value={char}
|
||||
maxLength={1}
|
||||
keyboardType={keyboardType}
|
||||
onChangeText={(text) => onCellChange(index, text)}
|
||||
onKeyPress={(event) => onCellKeyPress(index, event)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => {
|
||||
setFocusedIndex((current) =>
|
||||
current === index ? null : current
|
||||
);
|
||||
}}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
textContentType="oneTimeCode"
|
||||
inputMode="numeric"
|
||||
importantForAutofill="yes"
|
||||
selectionColor={colors.selected.accent}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
textAlign: "center",
|
||||
fontSize: AppFontSize.sm,
|
||||
color: colors.primary.paragraph,
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
blurOnSubmit={index === length - 1}
|
||||
returnKeyType={index === length - 1 ? "done" : "next"}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Hidden aggregate input for full-code paste/autofill and test compatibility. */}
|
||||
<TextInput
|
||||
testID={testID}
|
||||
value={cells.join("")}
|
||||
keyboardType={keyboardType}
|
||||
onChangeText={onBulkInputChange}
|
||||
maxLength={length}
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
textContentType="oneTimeCode"
|
||||
importantForAutofill="yes"
|
||||
style={{
|
||||
position: "absolute",
|
||||
opacity: 0,
|
||||
width: 1,
|
||||
height: 1
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinInput;
|
||||
@@ -18,24 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { DimensionValue, View } from "react-native";
|
||||
import { DimensionValue, View, ViewStyle } from "react-native";
|
||||
import { SvgXml } from "./lazy";
|
||||
import SvgImage from "react-native-svg/lib/typescript/elements/Image";
|
||||
export const SvgView = ({
|
||||
width = 250,
|
||||
height = 250,
|
||||
src
|
||||
src,
|
||||
style
|
||||
}: {
|
||||
width?: DimensionValue;
|
||||
height?: DimensionValue;
|
||||
src?: string;
|
||||
style?: ViewStyle;
|
||||
}) => {
|
||||
if (!src) return null;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: width || 250,
|
||||
width: height || 250
|
||||
width: height || 250,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<SvgXml xml={src} width="100%" height="100%" />
|
||||
|
||||
Reference in New Issue
Block a user