Skip to content

Commit

Permalink
[Mobile] [UMA-1096] Implement social login signing for send tez opera…
Browse files Browse the repository at this point in the history
…tion (#2368)
  • Loading branch information
OKendigelyan authored Feb 6, 2025
1 parent 300957f commit 7c421d8
Show file tree
Hide file tree
Showing 32 changed files with 1,508 additions and 97 deletions.
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

1 comment on commit 7c421d8

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.74% (1788/2135) 79.43% (850/1070) 78.27% (454/580)
apps/web Coverage: 83%
83.74% (1788/2135) 79.43% (850/1070) 78.27% (454/580)
packages/components Coverage: 97%
97.51% (196/201) 95.91% (94/98) 88.13% (52/59)
packages/core Coverage: 81%
82.37% (215/261) 72.51% (95/131) 81.66% (49/60)
packages/crypto Coverage: 100%
100% (43/43) 90.9% (10/11) 100% (7/7)
packages/data-polling Coverage: 96%
94.66% (142/150) 87.5% (21/24) 92.85% (39/42)
packages/multisig Coverage: 98%
98.47% (129/131) 85.71% (18/21) 100% (36/36)
packages/social-auth Coverage: 95%
95.45% (21/22) 91.66% (11/12) 100% (3/3)
packages/state Coverage: 83%
83.21% (833/1001) 79.58% (191/240) 76.7% (303/395)
packages/tezos Coverage: 89%
88.72% (118/133) 94.59% (35/37) 86.84% (33/38)
packages/tzkt Coverage: 89%
87.32% (62/71) 87.5% (14/16) 80.48% (33/41)

Please sign in to comment.