add register and login screens

This commit is contained in:
Jorrin
2024-04-20 00:11:02 +02:00
parent fcfd0d99cc
commit bbeb729156
20 changed files with 555 additions and 103 deletions

View File

@@ -41,6 +41,7 @@
"expo-av": "~13.10.5",
"expo-brightness": "~11.8.0",
"expo-build-properties": "~0.11.1",
"expo-clipboard": "^5.0.1",
"expo-constants": "~15.4.5",
"expo-file-system": "~16.0.8",
"expo-haptics": "~12.8.1",

View File

@@ -55,7 +55,7 @@ const TestDownloadButton = (props: {
<MaterialCommunityIcons
name="download"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={async () => {

View File

@@ -1,53 +1,12 @@
import { Link } from "expo-router";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { H2, H5, Paragraph, useTheme, View } from "tamagui";
import { H3, H5, Paragraph, View } from "tamagui";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card";
import { MWInput } from "~/components/ui/Input";
import { useAuth } from "~/hooks/useAuth";
import { useAuthStore } from "~/stores/settings";
function TestButtons() {
const theme = useTheme();
const { login } = useAuth();
return (
<View>
<MWButton
type="secondary"
backgroundColor="$sheetItemBackground"
marginBottom="$4"
icon={
<MaterialCommunityIcons
name="login"
size={24}
color={theme.buttonSecondaryText.val}
/>
}
onPress={async () => {
const passhphrase = "";
if (!passhphrase) {
alert("Please configure your passphrase");
return;
}
const account = await login({
mnemonic: passhphrase,
userData: {
device: "phone",
},
});
console.log(account);
}}
>
test login
</MWButton>
</View>
);
}
export default function MovieWebScreen() {
const { backendUrl, setBackendUrl } = useAuthStore();
@@ -59,16 +18,15 @@ export default function MovieWebScreen() {
justifyContent: "center",
}}
>
<TestButtons />
<MWCard bordered>
<MWCard.Header padded>
<H2 fontWeight="$bold" paddingBottom="$1">
<MWCard bordered padded>
<MWCard.Header>
<H3 fontWeight="$bold" paddingBottom="$1">
Sync to the cloud
</H2>
<H5 color="$ash50" fontWeight="$semibold" paddingVertical="$3">
</H3>
<H5 color="$shade200" fontWeight="$semibold" paddingVertical="$3">
Share your watch progress between devices and keep them synced.
</H5>
<Paragraph color="$ash50">
<Paragraph color="$shade200">
First choose the backend you want to use. If you do not know what
this does, use the default and click on &apos;Get started&apos;.
</Paragraph>
@@ -77,24 +35,24 @@ export default function MovieWebScreen() {
<View padding="$4">
<MWInput
placeholder={backendUrl}
type="search"
type="authentication"
value={backendUrl}
onChangeText={setBackendUrl}
/>
</View>
<MWCard.Footer padded justifyContent="center">
<MWButton type="purple">
<Link
href={{
pathname: "/sync/trust/[url]",
params: { url: backendUrl },
}}
style={{ color: "white", fontWeight: "bold" }}
>
<Link
href={{
pathname: "/sync/trust/[url]",
params: { url: backendUrl },
}}
asChild
>
<MWButton type="purple" width="100%">
Get started
</Link>
</MWButton>
</MWButton>
</Link>
</MWCard.Footer>
</MWCard>
</ScreenLayout>

View File

@@ -206,7 +206,7 @@ export default function SettingsScreen() {
android: "android",
})}
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
iconAfter={
@@ -229,7 +229,7 @@ export default function SettingsScreen() {
<MaterialCommunityIcons
name="broom"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => clearCacheDirectory()}
@@ -304,7 +304,7 @@ export function UpdateSheet({
<MaterialCommunityIcons
name={Platform.select({ ios: "apple", android: "android" })}
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => WebBrowser.openBrowserAsync(downloadUrl)}

View File

@@ -0,0 +1,79 @@
import { Stack } from "expo-router";
import { H4, Label, Paragraph, Text, YStack } from "tamagui";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card";
import { MWInput } from "~/components/ui/Input";
export default function Page() {
return (
<ScreenLayout
showHeader={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Stack.Screen
options={{
title: "",
}}
/>
<MWCard bordered padded>
<MWCard.Header>
<H4 fontWeight="$bold" textAlign="center">
Login to your account
</H4>
<Paragraph
color="$ash50"
textAlign="center"
fontWeight="$semibold"
paddingVertical="$4"
>
Please enter your passphrase to login to your account
</Paragraph>
</MWCard.Header>
<YStack paddingBottom="$5">
<YStack gap="$1">
<Label fontWeight="$bold">12-Word passphrase</Label>
<MWInput
type="authentication"
placeholder="Passphrase"
secureTextEntry
autoCorrect={false}
/>
</YStack>
<YStack gap="$1">
<Label fontWeight="$bold">Device name</Label>
<MWInput
type="authentication"
placeholder="Personal phone"
autoCorrect={false}
/>
</YStack>
</YStack>
<MWCard.Footer
padded
justifyContent="center"
flexDirection="column"
gap="$4"
>
<MWButton type="purple">Login</MWButton>
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
Don&apos;t have an account yet?{"\n"}
<Text color="$purple100" fontWeight="$bold">
Create an account.
</Text>
</Paragraph>
</MWCard.Footer>
</MWCard>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,190 @@
import { useState } from "react";
import { Link, Stack } from "expo-router";
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
import { Circle, H4, Label, Paragraph, View, XStack, YStack } from "tamagui";
import { LinearGradient } from "tamagui/linear-gradient";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card";
import { MWInput } from "~/components/ui/Input";
const colors = ["#0A54FF", "#CF2E68", "#F9DD7F", "#7652DD", "#2ECFA8"] as const;
function ColorPicker(props: {
value: (typeof colors)[number];
onInput: (v: (typeof colors)[number]) => void;
}) {
return (
<XStack gap="$2">
{colors.map((color) => {
return (
<View
onPress={() => props.onInput(color)}
flexGrow={1}
height="$4"
borderRadius="$4"
justifyContent="center"
alignItems="center"
backgroundColor={color}
key={color}
>
{props.value === color ? (
<Ionicons name="checkmark-circle" size={24} color="white" />
) : null}
</View>
);
})}
</XStack>
);
}
const icons = [
"user-group",
"couch",
"mobile-screen",
"ticket",
"handcuffs",
] as const;
function UserIconPicker(props: {
value: (typeof icons)[number];
onInput: (v: (typeof icons)[number]) => void;
}) {
return (
<XStack gap="$2">
{icons.map((icon) => {
return (
<View
flexGrow={1}
height="$4"
borderRadius="$4"
justifyContent="center"
alignItems="center"
backgroundColor={props.value === icon ? "$purple400" : "$shade400"}
borderColor={props.value === icon ? "$purple200" : "$shade400"}
borderWidth={1}
key={icon}
onPress={() => props.onInput(icon)}
>
<FontAwesome6 name={icon} size={24} color="white" />
</View>
);
})}
</XStack>
);
}
interface AvatarProps {
colorA: string;
colorB: string;
icon: (typeof icons)[number];
}
export function Avatar(props: AvatarProps) {
return (
<Circle
backgroundColor={props.colorA}
height="$6"
width="$6"
justifyContent="center"
alignItems="center"
>
<LinearGradient
colors={[props.colorA, props.colorB]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
borderRadius="$12"
width="100%"
height="100%"
justifyContent="center"
alignItems="center"
>
<FontAwesome6 name={props.icon} size={24} color="white" />
</LinearGradient>
</Circle>
);
}
export default function Page() {
const [color, setColor] = useState<(typeof colors)[number]>(colors[0]);
const [color2, setColor2] = useState<(typeof colors)[number]>(colors[0]);
const [icon, setIcon] = useState<(typeof icons)[number]>(icons[0]);
return (
<ScreenLayout
showHeader={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Stack.Screen
options={{
title: "",
}}
/>
<MWCard bordered padded>
<MWCard.Header>
<View alignItems="center" marginBottom="$3">
<Avatar colorA={color} colorB={color2} icon={icon} />
</View>
<H4 fontWeight="$bold" textAlign="center">
Account information
</H4>
<Paragraph
color="$shade200"
textAlign="center"
fontWeight="$normal"
paddingTop="$4"
>
Enter a name for your device and pick colours and a user icon of
your choosing
</Paragraph>
</MWCard.Header>
<YStack paddingBottom="$5">
<YStack gap="$1">
<Label fontWeight="$bold">Device name</Label>
<MWInput
type="authentication"
placeholder="Passphrase"
secureTextEntry
autoCorrect={false}
/>
</YStack>
<YStack gap="$1">
<Label fontWeight="$bold">Profile color one</Label>
<ColorPicker value={color} onInput={(color) => setColor(color)} />
</YStack>
<YStack gap="$1">
<Label fontWeight="$bold">Profile color two</Label>
<ColorPicker value={color2} onInput={(color) => setColor2(color)} />
</YStack>
<YStack gap="$1">
<Label fontWeight="$bold">User icon</Label>
<UserIconPicker value={icon} onInput={(icon) => setIcon(icon)} />
</YStack>
</YStack>
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
<Link
href={{
pathname: "/sync/register/confirm",
}}
replace
asChild
>
<MWButton type="purple" width="100%">
Next
</MWButton>
</Link>
</MWCard.Footer>
</MWCard>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,68 @@
import { Link, Stack } from "expo-router";
import { H4, Label, Paragraph, YStack } from "tamagui";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card";
import { MWInput } from "~/components/ui/Input";
export default function Page() {
return (
<ScreenLayout
showHeader={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Stack.Screen
options={{
title: "",
}}
/>
<MWCard bordered padded>
<MWCard.Header>
<H4 fontWeight="$bold" textAlign="center">
Confirm your passphrase
</H4>
<Paragraph
color="$shade200"
textAlign="center"
fontWeight="$normal"
paddingTop="$4"
>
Please enter your passphrase from earlier to confirm you have saved
it and to create your account
</Paragraph>
</MWCard.Header>
<YStack paddingBottom="$5">
<YStack gap="$1">
<Label fontWeight="$bold">12-Word passphrase</Label>
<MWInput
type="authentication"
placeholder="Passphrase"
secureTextEntry
autoCorrect={false}
/>
</YStack>
</YStack>
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
<Link
href={{
pathname: "/(tabs)/movie-web",
}}
replace
asChild
>
<MWButton type="purple">Create account</MWButton>
</Link>
</MWCard.Footer>
</MWCard>
</ScreenLayout>
);
}

View File

@@ -0,0 +1,132 @@
import { TouchableOpacity } from "react-native-gesture-handler";
import * as Clipboard from "expo-clipboard";
import { Link, Stack } from "expo-router";
import { Feather } from "@expo/vector-icons";
import { H4, Paragraph, Text, useTheme, View, XStack, YStack } from "tamagui";
import { genMnemonic } from "@movie-web/api";
import ScreenLayout from "~/components/layout/ScreenLayout";
import { MWButton } from "~/components/ui/Button";
import { MWCard } from "~/components/ui/Card";
function PassphraseWord({ word }: { word: string }) {
return (
<View
width="$10"
borderRadius="$4"
paddingHorizontal="$4"
paddingVertical="$3"
alignItems="center"
justifyContent="center"
backgroundColor="$shade400"
>
<Text fontWeight="$bold">{word}</Text>
</View>
);
}
export default function Page() {
const theme = useTheme();
const words = genMnemonic().split(" ");
return (
<ScreenLayout
showHeader={false}
contentContainerStyle={{
flexGrow: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Stack.Screen
options={{
title: "",
}}
/>
<MWCard bordered padded>
<MWCard.Header>
<H4 fontWeight="$bold" textAlign="center">
Your passphrase
</H4>
<Paragraph
color="$shade200"
textAlign="center"
fontWeight="$normal"
paddingTop="$4"
>
Your passphrase acts as your username and password. Make sure to
keep it safe as you will need to enter it to login to your account
</Paragraph>
</MWCard.Header>
<YStack
borderRadius="$4"
borderColor="$shade200"
borderWidth="$0.5"
marginBottom="$4"
>
<XStack
gap="$1"
borderBottomWidth="$0.5"
borderColor="$shade200"
paddingVertical="$2"
paddingHorizontal="$4"
>
<Text fontWeight="$bold" flexGrow={1}>
Passphrase
</Text>
<TouchableOpacity
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
}}
onPress={async () => {
await Clipboard.setStringAsync(words.join(""));
}}
>
<Feather name="copy" size={18} color={theme.shade200.val} />
<Text color="$shade200" fontWeight="$bold">
Copy
</Text>
</TouchableOpacity>
</XStack>
<View
flexWrap="wrap"
flexDirection="row"
gap="$4"
alignItems="center"
justifyContent="center"
padding="$3"
>
{words.map((word, index) => (
<PassphraseWord key={index} word={word} />
))}
</View>
</YStack>
<MWCard.Footer justifyContent="center" flexDirection="column" gap="$4">
<Link
href={{
pathname: "/sync/register/account",
}}
asChild
>
<MWButton type="purple">I have saved my passphrase</MWButton>
</Link>
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
Already have an account?{"\n"}
<Text color="$purple100" fontWeight="$bold">
<Link href="/sync/login">Login here</Link>
</Text>
</Paragraph>
</MWCard.Footer>
</MWCard>
</ScreenLayout>
);
}

View File

@@ -1,4 +1,4 @@
import { Stack, useLocalSearchParams } from "expo-router";
import { Link, Stack, useLocalSearchParams } from "expo-router";
import { useQuery } from "@tanstack/react-query";
import { H4, Paragraph, Text, View } from "tamagui";
@@ -30,7 +30,7 @@ export default function Page() {
title: "",
}}
/>
<MWCard bordered>
<MWCard bordered padded>
<MWCard.Header padded>
<H4 fontWeight="$bold" textAlign="center">
Do you trust this server?
@@ -90,15 +90,23 @@ export default function Page() {
flexDirection="column"
gap="$4"
>
<MWButton type="purple">I trust this server</MWButton>
<MWButton type="cancel">Go back</MWButton>
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
Already have an account?{" "}
<Text color="$purple100" fontWeight="$bold">
Login here
</Text>
</Paragraph>
<Link
href={{
pathname: "/sync/register",
}}
asChild
>
<MWButton type="purple">I trust this server</MWButton>
</Link>
<Link
href={{
pathname: "/(tabs)/",
}}
replace
asChild
>
<MWButton type="cancel">Go back</MWButton>
</Link>
</MWCard.Footer>
</MWCard>
</ScreenLayout>

View File

@@ -58,7 +58,7 @@ export const AudioTrackSelector = () => {
<MaterialCommunityIcons
name="volume-high"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => setOpen(true)}

View File

@@ -65,7 +65,7 @@ export const CaptionsSelector = () => {
<MaterialCommunityIcons
name="subtitles"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => setOpen(true)}

View File

@@ -38,7 +38,7 @@ export const DownloadButton = () => {
<MaterialCommunityIcons
name="download"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() =>

View File

@@ -47,7 +47,7 @@ const EpisodeSelector = ({
<Ionicons
name="arrow-back"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
onPress={() => {
setSelectedSeason(null);
props.onOpenChange?.(false);
@@ -119,7 +119,7 @@ export const SeasonSelector = () => {
<MaterialCommunityIcons
name="audio-video"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => setOpen(true)}

View File

@@ -23,7 +23,7 @@ export const SettingsSelector = () => {
<MaterialIcons
name="display-settings"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => setOpen(true)}

View File

@@ -102,7 +102,7 @@ const EmbedsPart = ({
<Ionicons
name="arrow-back"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
onPress={() => {
props.onOpenChange?.(false);
}}
@@ -160,7 +160,7 @@ export const SourceSelector = () => {
<MaterialCommunityIcons
name="video"
size={24}
color={theme.buttonSecondaryText.val}
color={theme.silver300.val}
/>
}
onPress={() => setOpen(true)}

View File

@@ -1,7 +1,7 @@
import { Card, styled, withStaticProperties } from "tamagui";
export const MWCardFrame = styled(Card, {
backgroundColor: "$shade400",
backgroundColor: "$shade600",
borderColor: "$shade400",
variants: {

View File

@@ -6,21 +6,37 @@ export const MWInput = styled(Input, {
variants: {
type: {
default: {
backgroundColor: "$inputBackground",
color: "$inputText",
placeholderTextColor: "$placeHolderText",
borderColor: "$inputBorder",
backgroundColor: "$ash600",
color: "$ash100",
placeholderTextColor: "$ash200",
borderColor: "$ash500",
outlineStyle: "none",
focusStyle: {
borderColor: "$ash300",
},
},
search: {
backgroundColor: "$searchBackground",
backgroundColor: "$shade500",
color: "$shade100",
borderColor: "$colorTransparent",
placeholderTextColor: "$searchPlaceholder",
placeholderTextColor: "$shade100",
outlineStyle: "none",
focusStyle: {
borderColor: "$colorTransparent",
},
},
authentication: {
backgroundColor: "$shade500",
color: "$shade100",
placeholderTextColor: "$shade400",
outlineStyle: "none",
focusStyle: {
borderColor: "$shade300",
},
pressStyle: {
backgroundColor: "$shade500",
},
},
},
},
});

View File

@@ -71,7 +71,7 @@ export function useAuth() {
backendUrl,
publicKeyBase64Url,
);
const signature = await signChallenge(keys, challenge);
const signature = signChallenge(keys, challenge);
const loginResult = await loginAccount(backendUrl, {
challenge: {
code: challenge,
@@ -110,7 +110,7 @@ export function useAuth() {
registerData.recaptchaToken,
);
const keys = await keysFromMnemonic(registerData.mnemonic);
const signature = await signChallenge(keys, challenge);
const signature = signChallenge(keys, challenge);
const registerResult = await registerAccount(backendUrl, {
challenge: {
code: challenge,

View File

@@ -57,17 +57,6 @@ const createThemeConfig = (tokens: Tokens) => ({
loadingIndicator: tokens.purple.c200,
buttonSecondaryBackground: tokens.ash.c700,
buttonSecondaryText: tokens.semantic.silver.c300,
buttonSecondaryBackgroundHover: tokens.ash.c700,
buttonPrimaryBackground: tokens.white,
buttonPrimaryText: tokens.black,
buttonPrimaryBackgroundHover: tokens.semantic.silver.c100,
buttonPurpleBackground: tokens.purple.c500,
buttonPurpleBackgroundHover: tokens.purple.c400,
buttonCancelBackground: tokens.ash.c500,
buttonCancelBackgroundHover: tokens.ash.c300,
switchActiveTrackColor: tokens.purple.c300,
switchInactiveTrackColor: tokens.ash.c500,
switchThumbColor: tokens.white,