mobile: update ui for auth screens

This commit is contained in:
Ammar Ahmed
2026-04-21 09:17:23 +05:00
committed by Abdullah Atta
parent d9127df1a3
commit e9711fd385
12 changed files with 930 additions and 587 deletions

View File

@@ -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
}}
/>
)}

View File

@@ -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>
</>
);

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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
};
};

View File

@@ -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>
)

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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}
</>
);
};

View 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;

View File

@@ -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%" />