mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 14:33:26 +00:00
add login, register, logout and devices list (including remove device)
This commit is contained in:
@@ -1,60 +1,10 @@
|
||||
import { Link } from "expo-router";
|
||||
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 { AccountInformation } from "~/components/account/AccountInformation";
|
||||
import { AccountGetStarted } from "~/components/account/GetStarted";
|
||||
import { useAuthStore } from "~/stores/settings";
|
||||
|
||||
export default function MovieWebScreen() {
|
||||
const { backendUrl, setBackendUrl } = useAuthStore();
|
||||
const account = useAuthStore((state) => state.account);
|
||||
|
||||
return (
|
||||
<ScreenLayout
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MWCard bordered padded>
|
||||
<MWCard.Header>
|
||||
<H3 fontWeight="$bold" paddingBottom="$1">
|
||||
Sync to the cloud
|
||||
</H3>
|
||||
<H5 color="$shade200" fontWeight="$semibold" paddingVertical="$3">
|
||||
Share your watch progress between devices and keep them synced.
|
||||
</H5>
|
||||
<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 'Get started'.
|
||||
</Paragraph>
|
||||
</MWCard.Header>
|
||||
|
||||
<View padding="$4">
|
||||
<MWInput
|
||||
placeholder={backendUrl}
|
||||
type="authentication"
|
||||
value={backendUrl}
|
||||
onChangeText={setBackendUrl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<MWCard.Footer padded justifyContent="center">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/sync/trust/[url]",
|
||||
params: { url: backendUrl },
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
<MWButton type="purple" width="100%">
|
||||
Get started
|
||||
</MWButton>
|
||||
</Link>
|
||||
</MWCard.Footer>
|
||||
</MWCard>
|
||||
</ScreenLayout>
|
||||
);
|
||||
if (account) return <AccountInformation />;
|
||||
return <AccountGetStarted />;
|
||||
}
|
||||
|
@@ -1,12 +1,40 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Link, Stack, useRouter } from "expo-router";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
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";
|
||||
import { useAuth } from "~/hooks/useAuth";
|
||||
import { useAuthStore } from "~/stores/settings";
|
||||
|
||||
export default function Page() {
|
||||
const backendUrl = useAuthStore((state) => state.backendUrl);
|
||||
const router = useRouter();
|
||||
const { login } = useAuth();
|
||||
|
||||
const [passphrase, setPassphrase] = useState("");
|
||||
const [deviceName, setDeviceName] = useState("");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationKey: ["login", backendUrl, passphrase, deviceName],
|
||||
mutationFn: () =>
|
||||
login({
|
||||
mnemonic: passphrase,
|
||||
userData: {
|
||||
device: deviceName,
|
||||
},
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
return router.push("/(tabs)/movie-web");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ScreenLayout
|
||||
showHeader={false}
|
||||
@@ -46,6 +74,8 @@ export default function Page() {
|
||||
placeholder="Passphrase"
|
||||
secureTextEntry
|
||||
autoCorrect={false}
|
||||
value={passphrase}
|
||||
onChangeText={setPassphrase}
|
||||
/>
|
||||
</YStack>
|
||||
<YStack gap="$1">
|
||||
@@ -54,6 +84,8 @@ export default function Page() {
|
||||
type="authentication"
|
||||
placeholder="Personal phone"
|
||||
autoCorrect={false}
|
||||
value={deviceName}
|
||||
onChangeText={setDeviceName}
|
||||
/>
|
||||
</YStack>
|
||||
</YStack>
|
||||
@@ -64,13 +96,22 @@ export default function Page() {
|
||||
flexDirection="column"
|
||||
gap="$4"
|
||||
>
|
||||
<MWButton type="purple">Login</MWButton>
|
||||
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||
Login
|
||||
</MWButton>
|
||||
{mutation.isError && (
|
||||
<Text color="$red100" textAlign="center">
|
||||
{mutation.error.message}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
||||
Don't have an account yet?{"\n"}
|
||||
<Text color="$purple100" fontWeight="$bold">
|
||||
Create an account.
|
||||
</Text>
|
||||
<Link href={{ pathname: "/sync/register" }} asChild>
|
||||
<Text color="$purple100" fontWeight="$bold">
|
||||
Create an account.
|
||||
</Text>
|
||||
</Link>
|
||||
</Paragraph>
|
||||
</MWCard.Footer>
|
||||
</MWCard>
|
||||
|
@@ -1,115 +1,43 @@
|
||||
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 { Stack, useRouter } from "expo-router";
|
||||
import { H4, Label, Paragraph, View, YStack } from "tamagui";
|
||||
|
||||
import { Avatar } from "~/components/account/Avatar";
|
||||
import { ColorPicker, colors } from "~/components/account/ColorPicker";
|
||||
import {
|
||||
expoIcons,
|
||||
expoIconsToDbIcons,
|
||||
UserIconPicker,
|
||||
} from "~/components/account/UserIconPicker";
|
||||
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]);
|
||||
const router = useRouter();
|
||||
|
||||
const [deviceName, setDeviceName] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [colorA, setColorA] = useState<(typeof colors)[number]>(colors[0]);
|
||||
const [colorB, setColorB] = useState<(typeof colors)[number]>(colors[0]);
|
||||
const [icon, setIcon] = useState<(typeof expoIcons)[number]>(expoIcons[0]);
|
||||
|
||||
const handleNext = () => {
|
||||
if (!deviceName) {
|
||||
setErrorMessage("Please enter a device name");
|
||||
return;
|
||||
}
|
||||
return router.push({
|
||||
pathname: "/sync/register/confirm",
|
||||
params: {
|
||||
deviceName,
|
||||
colorA,
|
||||
colorB,
|
||||
icon: expoIconsToDbIcons[icon],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenLayout
|
||||
@@ -129,7 +57,7 @@ export default function Page() {
|
||||
<MWCard bordered padded>
|
||||
<MWCard.Header>
|
||||
<View alignItems="center" marginBottom="$3">
|
||||
<Avatar colorA={color} colorB={color2} icon={icon} />
|
||||
<Avatar colorA={colorA} colorB={colorB} icon={icon} />
|
||||
</View>
|
||||
|
||||
<H4 fontWeight="$bold" textAlign="center">
|
||||
@@ -152,18 +80,19 @@ export default function Page() {
|
||||
<Label fontWeight="$bold">Device name</Label>
|
||||
<MWInput
|
||||
type="authentication"
|
||||
placeholder="Passphrase"
|
||||
secureTextEntry
|
||||
placeholder="Personal phone"
|
||||
autoCorrect={false}
|
||||
value={deviceName}
|
||||
onChangeText={setDeviceName}
|
||||
/>
|
||||
</YStack>
|
||||
<YStack gap="$1">
|
||||
<Label fontWeight="$bold">Profile color one</Label>
|
||||
<ColorPicker value={color} onInput={(color) => setColor(color)} />
|
||||
<ColorPicker value={colorA} onInput={(color) => setColorA(color)} />
|
||||
</YStack>
|
||||
<YStack gap="$1">
|
||||
<Label fontWeight="$bold">Profile color two</Label>
|
||||
<ColorPicker value={color2} onInput={(color) => setColor2(color)} />
|
||||
<ColorPicker value={colorB} onInput={(color) => setColorB(color)} />
|
||||
</YStack>
|
||||
<YStack gap="$1">
|
||||
<Label fontWeight="$bold">User icon</Label>
|
||||
@@ -172,17 +101,14 @@ export default function Page() {
|
||||
</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>
|
||||
{errorMessage && (
|
||||
<Paragraph color="$red100" textAlign="center">
|
||||
{errorMessage}
|
||||
</Paragraph>
|
||||
)}
|
||||
<MWButton type="purple" width="100%" onPress={handleNext}>
|
||||
Next
|
||||
</MWButton>
|
||||
</MWCard.Footer>
|
||||
</MWCard>
|
||||
</ScreenLayout>
|
||||
|
@@ -1,12 +1,45 @@
|
||||
import { Link, Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
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";
|
||||
import { useAuth } from "~/hooks/useAuth";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
const { deviceName, colorA, colorB, icon } = useLocalSearchParams<{
|
||||
deviceName: string;
|
||||
colorA: string;
|
||||
colorB: string;
|
||||
icon: string;
|
||||
}>();
|
||||
const { register } = useAuth();
|
||||
|
||||
const [passphrase, setPassphrase] = useState("");
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationKey: ["register", deviceName, colorA, colorB, icon],
|
||||
mutationFn: () =>
|
||||
register({
|
||||
// TODO: "Add recaptchaToken",
|
||||
mnemonic: passphrase,
|
||||
userData: {
|
||||
device: deviceName,
|
||||
profile: { colorA, colorB, icon },
|
||||
},
|
||||
}),
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
return router.push("/(tabs)/movie-web");
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ScreenLayout
|
||||
showHeader={false}
|
||||
@@ -47,20 +80,22 @@ export default function Page() {
|
||||
placeholder="Passphrase"
|
||||
secureTextEntry
|
||||
autoCorrect={false}
|
||||
value={passphrase}
|
||||
onChangeText={setPassphrase}
|
||||
/>
|
||||
</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>
|
||||
{mutation.isError && (
|
||||
<Paragraph color="$red100" textAlign="center">
|
||||
{mutation.error.message}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<MWButton type="purple" onPress={() => mutation.mutate()}>
|
||||
Create account
|
||||
</MWButton>
|
||||
</MWCard.Footer>
|
||||
</MWCard>
|
||||
</ScreenLayout>
|
||||
|
@@ -28,7 +28,7 @@ function PassphraseWord({ word }: { word: string }) {
|
||||
|
||||
export default function Page() {
|
||||
const theme = useTheme();
|
||||
const words = genMnemonic().split(" ");
|
||||
const words = genMnemonic();
|
||||
|
||||
return (
|
||||
<ScreenLayout
|
||||
@@ -86,7 +86,7 @@ export default function Page() {
|
||||
gap: 8,
|
||||
}}
|
||||
onPress={async () => {
|
||||
await Clipboard.setStringAsync(words.join(""));
|
||||
await Clipboard.setStringAsync(words);
|
||||
}}
|
||||
>
|
||||
<Feather name="copy" size={18} color={theme.shade200.val} />
|
||||
@@ -103,7 +103,7 @@ export default function Page() {
|
||||
justifyContent="center"
|
||||
padding="$3"
|
||||
>
|
||||
{words.map((word, index) => (
|
||||
{words.split(" ").map((word, index) => (
|
||||
<PassphraseWord key={index} word={word} />
|
||||
))}
|
||||
</View>
|
||||
@@ -122,7 +122,13 @@ export default function Page() {
|
||||
<Paragraph color="$ash50" textAlign="center" fontWeight="$semibold">
|
||||
Already have an account?{"\n"}
|
||||
<Text color="$purple100" fontWeight="$bold">
|
||||
<Link href="/sync/login">Login here</Link>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/sync/login",
|
||||
}}
|
||||
>
|
||||
Login here
|
||||
</Link>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</MWCard.Footer>
|
||||
|
@@ -7,13 +7,16 @@ import { getBackendMeta } from "@movie-web/api";
|
||||
import ScreenLayout from "~/components/layout/ScreenLayout";
|
||||
import { MWButton } from "~/components/ui/Button";
|
||||
import { MWCard } from "~/components/ui/Card";
|
||||
import { useAuthStore } from "~/stores/settings";
|
||||
|
||||
export default function Page() {
|
||||
const { url } = useLocalSearchParams<{ url: string }>();
|
||||
const { backendUrl } = useLocalSearchParams<{ backendUrl: string }>();
|
||||
|
||||
const setBackendUrl = useAuthStore((state) => state.setBackendUrl);
|
||||
|
||||
const meta = useQuery({
|
||||
queryKey: ["backendMeta", url],
|
||||
queryFn: () => getBackendMeta(url as unknown as string),
|
||||
queryKey: ["backendMeta", backendUrl],
|
||||
queryFn: () => getBackendMeta(backendUrl as unknown as string),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -52,7 +55,7 @@ export default function Page() {
|
||||
color="white"
|
||||
textDecorationLine="underline"
|
||||
>
|
||||
{url}
|
||||
{backendUrl}
|
||||
</Text>
|
||||
. Please confirm you trust it before making an account.
|
||||
</>
|
||||
@@ -95,8 +98,13 @@ export default function Page() {
|
||||
pathname: "/sync/register",
|
||||
}}
|
||||
asChild
|
||||
onPress={() => {
|
||||
setBackendUrl(backendUrl);
|
||||
}}
|
||||
>
|
||||
<MWButton type="purple">I trust this server</MWButton>
|
||||
<MWButton type="purple" disabled={!meta.isSuccess}>
|
||||
I trust this server
|
||||
</MWButton>
|
||||
</Link>
|
||||
<Link
|
||||
href={{
|
Reference in New Issue
Block a user