Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Mobile] [UMA-1096] Implement social login signing for send tez operation #2368

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/mobile/app/(auth)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Compass, Wallet } from "@tamagui/lucide-icons";
import { Tabs } from "expo-router";

export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarInactiveTintColor: "black",
headerShown: false,
}}
>
<Tabs.Screen
name="home"
options={{
title: "Home",
tabBarIcon: ({ color }) => <Wallet color={color} />,
}}
/>
<Tabs.Screen
name="explore"
options={{
title: "Explore",
tabBarIcon: ({ color }) => <Compass color={color} />,
}}
/>
</Tabs>
);
}
9 changes: 9 additions & 0 deletions apps/mobile/app/(auth)/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Text, YStack } from "tamagui";

export default function Explore() {
return (
<YStack alignItems="center" flex={1} paddingTop={20} backgroundColor="white">
<Text>Explore screen</Text>
</YStack>
);
}
9 changes: 9 additions & 0 deletions apps/mobile/app/(auth)/(tabs)/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useDataPolling } from "@umami/data-polling";

import { Home as HomeScreen } from "../../../screens/Home";

export default function Home() {
useDataPolling();

return <HomeScreen />;
}
5 changes: 0 additions & 5 deletions apps/mobile/app/(auth)/Home.tsx

This file was deleted.

11 changes: 9 additions & 2 deletions apps/mobile/app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { Stack } from "expo-router";
import { useCurrentAccount } from "@umami/state";
import { Redirect, Stack } from "expo-router";
import { View } from "react-native";

import { Header } from "../../components/Header";

export default function AuthenticatedLayout() {
const currentAccount = useCurrentAccount();

if (!currentAccount) {
return <Redirect href="/" />;
}

return (
<View style={{ flex: 1, paddingTop: 60, backgroundColor: "white", paddingHorizontal: 10 }}>
<Header />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="home" />
<Stack.Screen name="(tabs)" options={{ gestureEnabled: false }} />
</Stack>
</View>
);
Expand Down
36 changes: 21 additions & 15 deletions apps/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Toast, ToastProvider } from "@umami/utils";
import { Slot, SplashScreen } from "expo-router";
import { SplashScreen, Stack } from "expo-router";
import { useEffect } from "react";
import { useColorScheme } from "react-native";
import { Provider } from "react-redux";
Expand All @@ -8,7 +8,8 @@ import { TamaguiProvider } from "tamagui";

import { PersistorLoader } from "../components/PersistorLoader";
import { AuthProvider, ReactQueryProvider } from "../providers";
import store, { persistor } from "../store/store";
import { ModalProvider } from "../providers/ModalProvider";
import { persistor, store } from "../store";
import { tamaguiConfig } from "../tamagui.config";

export default function RootLayout() {
Expand All @@ -19,18 +20,23 @@ export default function RootLayout() {
}, []);

return (
<ToastProvider toast={{} as Toast}>
<ReactQueryProvider>
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme!}>
<Provider store={store}>
<PersistGate loading={<PersistorLoader />} persistor={persistor}>
<AuthProvider>
<Slot />
</AuthProvider>
</PersistGate>
</Provider>
</TamaguiProvider>
</ReactQueryProvider>
</ToastProvider>
<Provider store={store}>
<PersistGate loading={<PersistorLoader />} persistor={persistor}>
<ToastProvider toast={{} as Toast}>
<ReactQueryProvider>
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme!}>
<ModalProvider>
<AuthProvider>
<Stack screenOptions={{ headerShown: false, gestureEnabled: false }}>
<Stack.Screen name="index" />
<Stack.Screen name="(auth)" />
</Stack>
</AuthProvider>
</ModalProvider>
</TamaguiProvider>
</ReactQueryProvider>
</ToastProvider>
</PersistGate>
</Provider>
);
}
File renamed without changes.
25 changes: 25 additions & 0 deletions apps/mobile/components/ModalBackButton/ModalBackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ArrowLeft } from "@tamagui/lucide-icons";
import { Button, styled } from "tamagui";

import { useModal } from "../../providers/ModalProvider";

type ModalBackButtonProps = {
goBack?: () => void;
};

export const ModalBackButton = ({ goBack }: ModalBackButtonProps) => {
const { hideModal } = useModal();

return <BackButton icon={<ArrowLeft />} onPress={goBack ?? hideModal} />;
};

const BackButton = styled(Button, {
position: "absolute",
top: 12,
left: 12,
zIndex: 1000,
borderRadius: 100,
width: "auto",
height: "auto",
padding: 10,
});
1 change: 1 addition & 0 deletions apps/mobile/components/ModalBackButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ModalBackButton";
21 changes: 21 additions & 0 deletions apps/mobile/components/ModalCloseButton/ModalCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { X } from "@tamagui/lucide-icons";
import { Button, styled } from "tamagui";

import { useModal } from "../../providers/ModalProvider";

export const ModalCloseButton = () => {
const { hideModal } = useModal();

return <CloseButton icon={<X />} onPress={hideModal} />;
};

const CloseButton = styled(Button, {
position: "absolute",
top: 12,
right: 12,
zIndex: 1000,
borderRadius: 100,
width: "auto",
height: "auto",
padding: 10,
});
1 change: 1 addition & 0 deletions apps/mobile/components/ModalCloseButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ModalCloseButton";
153 changes: 153 additions & 0 deletions apps/mobile/components/SendFlow/SignButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { type TezosToolkit } from "@taquito/taquito";
import type { BatchWalletOperation } from "@taquito/taquito/dist/types/wallet/batch-operation";
import {
type ImplicitAccount,
type LedgerAccount,
type MnemonicAccount,
type SecretKeyAccount,
type SocialAccount,
} from "@umami/core";
import { useAsyncActionHandler, useGetSecretKey, useSelectedNetwork } from "@umami/state";
import { type Network, makeToolkit } from "@umami/tezos";
import { useCustomToast } from "@umami/utils";
import { FormProvider, useForm, useFormContext } from "react-hook-form";
import { Button, Input, Label, Spinner, Text, View, YStack } from "tamagui";

import { forIDP } from "../../services/auth";

export const SignButton = ({
signer,
onSubmit,
isLoading: externalIsLoading,
isDisabled,
text = "Confirm Transaction",
network: preferredNetwork,
}: {
onSubmit: (tezosToolkit: TezosToolkit) => Promise<BatchWalletOperation | void>;
signer: ImplicitAccount;
isLoading?: boolean;
isDisabled?: boolean;
text?: string; // TODO: after FillStep migration change to the header value from SignPage
network?: Network;
}) => {
const form = useForm<{ password: string }>({ mode: "onBlur", defaultValues: { password: "" } });
const {
handleSubmit,
formState: { errors, isValid: isPasswordValid },
} = form;
let network = useSelectedNetwork();
if (preferredNetwork) {
network = preferredNetwork;
}

const {
formState: { isValid: isOuterFormValid },
} = useFormContext();

const isButtonDisabled = isDisabled || !isPasswordValid || !isOuterFormValid;

const getSecretKey = useGetSecretKey();
const toast = useCustomToast();
const { isLoading: internalIsLoading, handleAsyncAction } = useAsyncActionHandler();
const isLoading = internalIsLoading || externalIsLoading;

const onMnemonicSign = async ({ password }: { password: string }) =>
handleAsyncAction(async () => {
const secretKey = await getSecretKey(signer as MnemonicAccount, password);
return onSubmit(await makeToolkit({ type: "mnemonic", secretKey, network }));
});

const onSecretKeySign = async ({ password }: { password: string }) =>
handleAsyncAction(async () => {
const secretKey = await getSecretKey(signer as SecretKeyAccount, password);
return onSubmit(await makeToolkit({ type: "secret_key", secretKey, network }));
});

const onSocialSign = async () =>
handleAsyncAction(async () => {
const { secretKey } = await forIDP((signer as SocialAccount).idp).getCredentials();
return onSubmit(await makeToolkit({ type: "social", secretKey, network }));
});

const onLedgerSign = async () =>
handleAsyncAction(
async () => {
toast({
id: "ledger-sign-toast",
description: "Please approve the operation on your Ledger",
status: "info",
duration: 60000,
isClosable: true,
});
return onSubmit(
await makeToolkit({
type: "ledger",
account: signer as LedgerAccount,
network,
})
);
},
(error: any) => ({
description: `${error.message} Please connect your ledger, open Tezos app and try submitting transaction again`,
status: "error",
})
).finally(() => toast.close("ledger-sign-toast"));

switch (signer.type) {
case "secret_key":
case "mnemonic":
return (
<View width="100%">
<FormProvider {...form}>
<YStack alignItems="start" spacing="30px">
<YStack isInvalid={!!errors.password}>
<Label>Password</Label>
<Input
data-testid="password"
type="password"
{...form.register("password", { required: "Password is required" })}
/>
{errors.password && <Text>{errors.password.message}</Text>}
</YStack>
<Button
width="100%"
disabled={isButtonDisabled || isLoading}
icon={isLoading ? <Spinner color="$green10" size="small" /> : null}
onPress={handleSubmit(
signer.type === "mnemonic" ? onMnemonicSign : onSecretKeySign
)}
type="submit"
variant="primary"
>
{text}
</Button>
</YStack>
</FormProvider>
</View>
);
case "social":
return (
<Button
width="100%"
disabled={isDisabled || isLoading}
icon={isLoading ? <Spinner color="$green10" size="small" /> : null}
onPress={onSocialSign}
variant="primary"
>
{text}
</Button>
);
case "ledger":
return (
<Button
width="100%"
disabled={isDisabled || isLoading}
icon={isLoading ? <Spinner color="$green10" size="small" /> : null}
onPress={onLedgerSign}
variant="primary"
>
{text}
</Button>
);
}
};
Loading
Loading