diff --git a/apps/mobile/app/(auth)/(tabs)/_layout.tsx b/apps/mobile/app/(auth)/(tabs)/_layout.tsx
new file mode 100644
index 0000000000..e1a59aee1c
--- /dev/null
+++ b/apps/mobile/app/(auth)/(tabs)/_layout.tsx
@@ -0,0 +1,28 @@
+import { Compass, Wallet } from "@tamagui/lucide-icons";
+import { Tabs } from "expo-router";
+
+export default function TabLayout() {
+ return (
+
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+
+ );
+}
diff --git a/apps/mobile/app/(auth)/(tabs)/explore.tsx b/apps/mobile/app/(auth)/(tabs)/explore.tsx
new file mode 100644
index 0000000000..6de52f0f3c
--- /dev/null
+++ b/apps/mobile/app/(auth)/(tabs)/explore.tsx
@@ -0,0 +1,9 @@
+import { Text, YStack } from "tamagui";
+
+export default function Explore() {
+ return (
+
+ Explore screen
+
+ );
+}
diff --git a/apps/mobile/app/(auth)/(tabs)/home.tsx b/apps/mobile/app/(auth)/(tabs)/home.tsx
new file mode 100644
index 0000000000..f2b3a4c39c
--- /dev/null
+++ b/apps/mobile/app/(auth)/(tabs)/home.tsx
@@ -0,0 +1,9 @@
+import { useDataPolling } from "@umami/data-polling";
+
+import { Home as HomeScreen } from "../../../screens/Home";
+
+export default function Home() {
+ useDataPolling();
+
+ return ;
+}
diff --git a/apps/mobile/app/(auth)/Home.tsx b/apps/mobile/app/(auth)/Home.tsx
deleted file mode 100644
index 1b900f8230..0000000000
--- a/apps/mobile/app/(auth)/Home.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Home as HomeScreen } from "../../screens/Home";
-
-export default function Home() {
- return ;
-}
diff --git a/apps/mobile/app/(auth)/_layout.tsx b/apps/mobile/app/(auth)/_layout.tsx
index 159499661c..dd2ba2c678 100644
--- a/apps/mobile/app/(auth)/_layout.tsx
+++ b/apps/mobile/app/(auth)/_layout.tsx
@@ -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 ;
+ }
+
return (
-
+
);
diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx
index 64f4398e9c..3328f7f9c7 100644
--- a/apps/mobile/app/_layout.tsx
+++ b/apps/mobile/app/_layout.tsx
@@ -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";
@@ -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() {
@@ -19,18 +20,23 @@ export default function RootLayout() {
}, []);
return (
-
-
-
-
- } persistor={persistor}>
-
-
-
-
-
-
-
-
+
+ } persistor={persistor}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/mobile/app/login.tsx b/apps/mobile/app/index.tsx
similarity index 100%
rename from apps/mobile/app/login.tsx
rename to apps/mobile/app/index.tsx
diff --git a/apps/mobile/components/ModalBackButton/ModalBackButton.tsx b/apps/mobile/components/ModalBackButton/ModalBackButton.tsx
new file mode 100644
index 0000000000..f1c33e61d6
--- /dev/null
+++ b/apps/mobile/components/ModalBackButton/ModalBackButton.tsx
@@ -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 } onPress={goBack ?? hideModal} />;
+};
+
+const BackButton = styled(Button, {
+ position: "absolute",
+ top: 12,
+ left: 12,
+ zIndex: 1000,
+ borderRadius: 100,
+ width: "auto",
+ height: "auto",
+ padding: 10,
+});
diff --git a/apps/mobile/components/ModalBackButton/index.ts b/apps/mobile/components/ModalBackButton/index.ts
new file mode 100644
index 0000000000..8820477800
--- /dev/null
+++ b/apps/mobile/components/ModalBackButton/index.ts
@@ -0,0 +1 @@
+export * from "./ModalBackButton";
diff --git a/apps/mobile/components/ModalCloseButton/ModalCloseButton.tsx b/apps/mobile/components/ModalCloseButton/ModalCloseButton.tsx
new file mode 100644
index 0000000000..d02c5286a4
--- /dev/null
+++ b/apps/mobile/components/ModalCloseButton/ModalCloseButton.tsx
@@ -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 } onPress={hideModal} />;
+};
+
+const CloseButton = styled(Button, {
+ position: "absolute",
+ top: 12,
+ right: 12,
+ zIndex: 1000,
+ borderRadius: 100,
+ width: "auto",
+ height: "auto",
+ padding: 10,
+});
diff --git a/apps/mobile/components/ModalCloseButton/index.ts b/apps/mobile/components/ModalCloseButton/index.ts
new file mode 100644
index 0000000000..9bdd43a704
--- /dev/null
+++ b/apps/mobile/components/ModalCloseButton/index.ts
@@ -0,0 +1 @@
+export * from "./ModalCloseButton";
diff --git a/apps/mobile/components/SendFlow/SignButton.tsx b/apps/mobile/components/SendFlow/SignButton.tsx
new file mode 100644
index 0000000000..9821f7d722
--- /dev/null
+++ b/apps/mobile/components/SendFlow/SignButton.tsx
@@ -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;
+ 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 (
+
+
+
+
+
+
+ {errors.password && {errors.password.message}}
+
+ : null}
+ onPress={handleSubmit(
+ signer.type === "mnemonic" ? onMnemonicSign : onSecretKeySign
+ )}
+ type="submit"
+ variant="primary"
+ >
+ {text}
+
+
+
+
+ );
+ case "social":
+ return (
+ : null}
+ onPress={onSocialSign}
+ variant="primary"
+ >
+ {text}
+
+ );
+ case "ledger":
+ return (
+ : null}
+ onPress={onLedgerSign}
+ variant="primary"
+ >
+ {text}
+
+ );
+ }
+};
diff --git a/apps/mobile/components/SendFlow/SuccessStep/SuccessStep.tsx b/apps/mobile/components/SendFlow/SuccessStep/SuccessStep.tsx
new file mode 100644
index 0000000000..751d740cca
--- /dev/null
+++ b/apps/mobile/components/SendFlow/SuccessStep/SuccessStep.tsx
@@ -0,0 +1,67 @@
+import { Check, ExternalLink } from "@tamagui/lucide-icons";
+import { useSelectedNetwork } from "@umami/state";
+import * as Linking from "expo-linking";
+import { useRouter } from "expo-router";
+import { Button, Dialog, Text, YStack } from "tamagui";
+
+type SuccessStepProps = {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ hash: string;
+};
+
+export const SuccessStep = ({ open, onOpenChange, hash }: SuccessStepProps) => {
+ const network = useSelectedNetwork();
+ const router = useRouter();
+ const tzktUrl = `${network.tzktExplorerUrl}/${hash}`;
+
+ const handleViewOperations = () => {
+ onOpenChange(false);
+ router.push("/home");
+ };
+
+ const handleViewInTzkt = async () => {
+ await Linking.openURL(tzktUrl);
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/mobile/components/SendFlow/SuccessStep/index.ts b/apps/mobile/components/SendFlow/SuccessStep/index.ts
new file mode 100644
index 0000000000..d1b42a3c70
--- /dev/null
+++ b/apps/mobile/components/SendFlow/SuccessStep/index.ts
@@ -0,0 +1 @@
+export * from "./SuccessStep";
diff --git a/apps/mobile/components/SendFlow/Tez/FormPage.tsx b/apps/mobile/components/SendFlow/Tez/FormPage.tsx
new file mode 100644
index 0000000000..41b642b111
--- /dev/null
+++ b/apps/mobile/components/SendFlow/Tez/FormPage.tsx
@@ -0,0 +1,148 @@
+import { type TezTransfer, getSmallestUnit } from "@umami/core";
+import { useGetAccountBalance, useTezToDollar } from "@umami/state";
+import {
+ type RawPkh,
+ TEZ_DECIMALS,
+ mutezToTez,
+ parsePkh,
+ prettyTezAmount,
+ tezToMutez,
+} from "@umami/tezos";
+import { Controller, FormProvider, useForm } from "react-hook-form";
+import { Form, Input, Label, Text, XStack, YStack } from "tamagui";
+
+import { SignPage } from "./SignPage";
+import { ModalCloseButton } from "../../ModalCloseButton";
+import {
+ useAddToBatchFormAction,
+ useHandleOnSubmitFormActions,
+ useOpenSignPageFormAction,
+} from "../onSubmitFormActionHooks";
+import {
+ type FormPageProps,
+ FormSubmitButton,
+ formDefaultValues,
+ makeValidateDecimals,
+} from "../utils";
+
+type FormValues = {
+ sender: RawPkh;
+ recipient: RawPkh;
+ prettyAmount: string;
+};
+
+export const FormPage = ({ ...props }: FormPageProps) => {
+ const getBalance = useGetAccountBalance();
+ const tezToDollar = useTezToDollar();
+
+ const getCurrentBalance = () => {
+ if (props.sender?.address.pkh) {
+ return getBalance(props.sender.address.pkh);
+ }
+ };
+
+ const getDollarBalance = () => {
+ if (getCurrentBalance() === undefined) {
+ return undefined;
+ }
+
+ const usdBalance = tezToDollar(mutezToTez(getCurrentBalance()!).toFixed());
+ return usdBalance !== undefined && `$${usdBalance}`;
+ };
+
+ const openSignPage = useOpenSignPageFormAction({
+ SignPage,
+ FormPage,
+ defaultFormPageProps: props,
+ toOperation,
+ });
+
+ const addToBatch = useAddToBatchFormAction(toOperation);
+
+ const {
+ onFormSubmitActionHandlers: [onSingleSubmit],
+ isLoading,
+ } = useHandleOnSubmitFormActions([openSignPage, addToBatch]);
+
+ const form = useForm({
+ defaultValues: formDefaultValues(props),
+ mode: "onBlur",
+ });
+
+ const {
+ formState: { errors },
+ handleSubmit,
+ } = form;
+
+ return (
+
+
+
+ );
+};
+
+const toOperation = (formValues: FormValues): TezTransfer => ({
+ type: "tez",
+ amount: tezToMutez(formValues.prettyAmount).toFixed(),
+ recipient: parsePkh(formValues.recipient),
+});
diff --git a/apps/mobile/components/SendFlow/Tez/SignPage.tsx b/apps/mobile/components/SendFlow/Tez/SignPage.tsx
new file mode 100644
index 0000000000..c400ca4fad
--- /dev/null
+++ b/apps/mobile/components/SendFlow/Tez/SignPage.tsx
@@ -0,0 +1,60 @@
+import { type TezTransfer } from "@umami/core";
+import { prettyTezAmount } from "@umami/tezos";
+import { FormProvider } from "react-hook-form";
+import { Form, Text, XStack, YStack } from "tamagui";
+
+import { ModalBackButton } from "../../ModalBackButton";
+import { ModalCloseButton } from "../../ModalCloseButton";
+import { SignButton } from "../SignButton";
+import { type SignPageProps, useSignPageHelpers } from "../utils";
+
+export const SignPage = (props: SignPageProps) => {
+ const { fee, operations, estimationFailed, isLoading, form, signer, onSign } = useSignPageHelpers(
+ props.operations
+ );
+
+ const { amount: mutezAmount, recipient } = operations.operations[0] as TezTransfer;
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/mobile/components/SendFlow/Tez/index.ts b/apps/mobile/components/SendFlow/Tez/index.ts
new file mode 100644
index 0000000000..297f1b36d8
--- /dev/null
+++ b/apps/mobile/components/SendFlow/Tez/index.ts
@@ -0,0 +1,2 @@
+export * from "./FormPage";
+export * from "./SignPage";
diff --git a/apps/mobile/components/SendFlow/onSubmitFormActionHooks.tsx b/apps/mobile/components/SendFlow/onSubmitFormActionHooks.tsx
new file mode 100644
index 0000000000..03468d2b18
--- /dev/null
+++ b/apps/mobile/components/SendFlow/onSubmitFormActionHooks.tsx
@@ -0,0 +1,148 @@
+import { useToast } from "@chakra-ui/react";
+import { type EstimatedAccountOperations, type Operation, estimate } from "@umami/core";
+import {
+ estimateAndUpdateBatch,
+ useAppDispatch,
+ useAsyncActionHandler,
+ useSelectedNetwork,
+} from "@umami/state";
+import { type FunctionComponent } from "react";
+
+import {
+ type BaseFormValues,
+ type FormPageProps,
+ type SignPageProps,
+ useMakeFormOperations,
+} from "./utils";
+import { useModal } from "../../providers/ModalProvider";
+
+// This file defines hooks to create actions when form is submitted.
+
+type OnSubmitFormAction = (
+ formValues: FormValues
+) => Promise;
+
+type UseOpenSignPageArgs<
+ ExtraData,
+ FormValues extends BaseFormValues,
+ FormProps extends FormPageProps,
+> = {
+ // Sign page component to render.
+ SignPage: FunctionComponent>;
+ // Extra data to pass to the Sign page component (e.g. NFT or Token)
+ signPageExtraData?: ExtraData;
+ // Form page component to render when the user goes back from the sign page.
+ FormPage: FunctionComponent;
+ // Form page props, used to render the form page again when the user goes back from the sign page
+ defaultFormPageProps: FormProps;
+ // Function to convert raw form values to the Operation type we can work with
+ // to submit operations.
+ toOperation: (formValues: FormValues) => Operation;
+};
+
+// Hook to open the sign page that knows how to get back to the form page.
+export const useOpenSignPageFormAction = <
+ SignPageData,
+ FormValues extends BaseFormValues,
+ FormProps extends FormPageProps,
+>({
+ SignPage,
+ signPageExtraData,
+ FormPage,
+ defaultFormPageProps,
+ toOperation,
+}: UseOpenSignPageArgs): OnSubmitFormAction => {
+ const makeFormOperations = useMakeFormOperations(toOperation);
+ const network = useSelectedNetwork();
+ const { showModal } = useModal();
+
+ return async (formValues: FormValues) => {
+ try {
+ console.log(formValues, network);
+ const operations = makeFormOperations(formValues);
+ const estimatedOperations = await estimate(operations, network);
+ return showModal(
+
+ showModal(
+
+ )
+ }
+ mode="single"
+ operations={estimatedOperations}
+ />
+ );
+ } catch (e) {
+ console.log(e);
+ }
+ };
+};
+
+export const useAddToBatchFormAction = (
+ toOperation: (formValues: FormValues) => Operation
+): OnSubmitFormAction => {
+ const network = useSelectedNetwork();
+ const makeFormOperations = useMakeFormOperations(toOperation);
+ const dispatch = useAppDispatch();
+ const toast = useToast();
+
+ const onAddToBatchAction = async (formValues: FormValues) => {
+ const operations = makeFormOperations(formValues);
+ await dispatch(estimateAndUpdateBatch(operations, network));
+ toast({ description: "Transaction added to batch!", status: "success" });
+ // onClose();
+ };
+
+ return onAddToBatchAction;
+};
+
+// Wraps the OnSubmitFormActions in a async action handler that shows a toast if the action fails.
+// If any of the actions is loading then isLoading will be true.
+export const useHandleOnSubmitFormActions = (
+ onSubmitFormActions: OnSubmitFormAction[]
+) => {
+ const { handleAsyncAction, isLoading } = useAsyncActionHandler();
+
+ const onFormSubmitActionHandlers = onSubmitFormActions.map(
+ action => async (formValues: FormValues) => handleAsyncAction(() => action(formValues))
+ );
+
+ return {
+ onFormSubmitActionHandlers,
+ isLoading,
+ };
+};
+
+export function usePreviewOperations<
+ FormValues extends BaseFormValues,
+ SignPageProps extends { operations: EstimatedAccountOperations } = {
+ operations: EstimatedAccountOperations;
+ },
+>(
+ toOperation: (formValues: FormValues) => Operation | Operation[],
+ SignPage: FunctionComponent,
+ props: Omit
+) {
+ const network = useSelectedNetwork();
+ const makeFormOperations = useMakeFormOperations(toOperation);
+ const { handleAsyncAction, isLoading } = useAsyncActionHandler();
+ const { showModal } = useModal();
+
+ return {
+ isLoading,
+ previewOperation: (formValues: FormValues) =>
+ handleAsyncAction(async () => {
+ const operations = makeFormOperations(formValues);
+ const estimatedOperations = await estimate(operations, network);
+
+ return showModal(
+
+ );
+ }),
+ };
+}
+export const usePreviewOperation = usePreviewOperations;
diff --git a/apps/mobile/components/SendFlow/types.ts b/apps/mobile/components/SendFlow/types.ts
new file mode 100644
index 0000000000..bdd8acb89e
--- /dev/null
+++ b/apps/mobile/components/SendFlow/types.ts
@@ -0,0 +1,9 @@
+import { type Operations } from "@umami/core";
+
+export interface SignDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ operations: Operations;
+ mode: "single" | "batch";
+ data?: unknown;
+}
diff --git a/apps/mobile/components/SendFlow/utils.tsx b/apps/mobile/components/SendFlow/utils.tsx
new file mode 100644
index 0000000000..7740e412da
--- /dev/null
+++ b/apps/mobile/components/SendFlow/utils.tsx
@@ -0,0 +1,244 @@
+import { type SigningType } from "@airgap/beacon-wallet";
+import { type TezosToolkit } from "@taquito/taquito";
+import {
+ type Account,
+ type AccountOperations,
+ type EstimatedAccountOperations,
+ type ImplicitAccount,
+ type Operation,
+ estimate,
+ executeOperations,
+ makeAccountOperations,
+ totalFee,
+} from "@umami/core";
+import {
+ useAsyncActionHandler,
+ useGetBestSignerForAccount,
+ useGetImplicitAccount,
+ useGetOwnedAccount,
+ useSelectedNetwork,
+} from "@umami/state";
+import { type ExecuteParams, type Network, type RawPkh } from "@umami/tezos";
+import * as WebBrowser from "expo-web-browser";
+import { repeat } from "lodash";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { Alert } from "react-native";
+import { Button, type ButtonProps, Spinner } from "tamagui";
+
+import { useModal } from "../../providers/ModalProvider";
+
+// Convert given optional fields to required
+// For example:
+// type A = {a?:number, b:string}
+// RequiredFields === {a:number, b:string}
+type RequiredFields = Omit & Required>;
+
+export type FormPageProps = { sender?: Account; form?: T };
+
+// FormPagePropsWithSender is the same as FormPageProps but with sender required,
+// Use this when we don't want to give the users options to select the sender
+// (e.g. the nft and token form)
+export type FormPagePropsWithSender = RequiredFields, "sender">;
+
+// Form values should always have a sender field.
+export type BaseFormValues = { sender: RawPkh };
+
+export type SignPageMode = "single" | "batch";
+
+export type SignPageProps = {
+ goBack?: () => void;
+ operations: EstimatedAccountOperations;
+ mode: SignPageMode;
+ data?: T;
+};
+
+export type CalculatedSignProps = {
+ fee: number;
+ isSigning: boolean;
+ onSign: (tezosToolkit: TezosToolkit) => Promise;
+ network: any;
+};
+
+export type sdkType = "beacon" | "walletconnect";
+
+export type SignRequestId =
+ | {
+ sdkType: "beacon";
+ id: string;
+ }
+ | {
+ sdkType: "walletconnect";
+ id: number;
+ topic: string;
+ };
+
+export type SignHeaderProps = {
+ network: Network;
+ appName: string;
+ appIcon?: string;
+ isScam?: boolean;
+ validationStatus?: "VALID" | "INVALID" | "UNKNOWN";
+ requestId: SignRequestId;
+};
+
+export type SdkSignPageProps = {
+ operation: EstimatedAccountOperations;
+ headerProps: SignHeaderProps;
+};
+
+export type SignPayloadProps = {
+ requestId: SignRequestId;
+ appName: string;
+ appIcon?: string;
+ payload: string;
+ isScam?: boolean;
+ validationStatus?: "VALID" | "INVALID" | "UNKNOWN";
+ signer: ImplicitAccount;
+ signingType: SigningType;
+};
+
+export const FormSubmitButton = ({ title = "Preview", ...props }: ButtonProps) => (
+ : null}
+ type="submit"
+ variant="primary"
+ {...props}
+ >
+ {title}
+
+);
+
+export const formDefaultValues = ({ sender, form }: FormPageProps) => {
+ if (form) {
+ return form;
+ } else if (sender) {
+ return { sender: sender.address.pkh };
+ } else {
+ return {};
+ }
+};
+
+// TODO: test this
+export const useSignPageHelpers = (
+ // the fee & operations you've got from the form
+ initialOperations: EstimatedAccountOperations
+) => {
+ const [estimationFailed, setEstimationFailed] = useState(false);
+ const getSigner = useGetImplicitAccount();
+ const [operations, setOperations] = useState(initialOperations);
+ const network = useSelectedNetwork();
+ const { isLoading, handleAsyncAction, handleAsyncActionUnsafe } = useAsyncActionHandler();
+ // const { openWith } = useDynamicModalContext();
+ const { hideModal } = useModal();
+
+ const form = useForm<{
+ sender: string;
+ signer: string;
+ executeParams: ExecuteParams[];
+ }>({
+ mode: "onBlur",
+ defaultValues: {
+ signer: operations.signer.address.pkh,
+ sender: operations.sender.address.pkh,
+ executeParams: operations.estimates,
+ },
+ });
+
+ const signer = form.watch("signer");
+
+ // if it fails then the sign button must be disabled
+ // and the user is supposed to either come back to the form and amend it
+ // or choose another signer
+ const reEstimate = async (newSigner: RawPkh) =>
+ handleAsyncActionUnsafe(
+ async () => {
+ const newOperations = await estimate(
+ {
+ ...operations,
+ signer: getSigner(newSigner),
+ },
+ network
+ );
+ form.setValue("executeParams", newOperations.estimates);
+ setOperations(newOperations);
+ setEstimationFailed(false);
+ },
+ {
+ isClosable: true,
+ duration: null, // it makes the toast stick until the user closes it
+ }
+ ).catch(() => setEstimationFailed(true));
+
+ const onSign = async (tezosToolkit: TezosToolkit) =>
+ handleAsyncAction(async () => {
+ const operation = await executeOperations(
+ { ...operations, estimates: form.watch("executeParams") },
+ tezosToolkit
+ );
+ // await openWith();
+ hideModal();
+ Alert.alert(
+ "Operation Submitted",
+ "You can follow this operation's progress in the Operations section. \nIt may take up to 30 seconds to appear.",
+ [
+ {
+ text: "Close",
+ },
+ {
+ text: "Open in Explorer",
+ onPress: () => {
+ void WebBrowser.openBrowserAsync(`${network.tzktExplorerUrl}/${operation.opHash}`);
+ },
+ },
+ ]
+ );
+
+ return operation;
+ });
+
+ return {
+ fee: totalFee(form.watch("executeParams")),
+ estimationFailed,
+ operations,
+ isLoading,
+ form,
+ signer: getSigner(signer),
+ reEstimate,
+ onSign,
+ };
+};
+
+export const useMakeFormOperations = (
+ toOperation: (formValues: FormValues) => Operation | Operation[]
+): ((formValues: FormValues) => AccountOperations) => {
+ const getAccount = useGetOwnedAccount();
+ const getSigner = useGetBestSignerForAccount();
+
+ return (formValues: FormValues) => {
+ const sender = getAccount(formValues.sender);
+ return makeAccountOperations(sender, getSigner(sender), [toOperation(formValues)].flat());
+ };
+};
+
+export const getSmallestUnit = (decimals: number): string => {
+ if (decimals < 0) {
+ console.warn("Decimals cannot be negative");
+ decimals = 0;
+ }
+
+ const leadingZeroes = decimals === 0 ? "" : "0." + repeat("0", decimals - 1);
+ return `${leadingZeroes}1`;
+};
+
+export const makeValidateDecimals = (decimals: number) => (val: string) => {
+ if (val.includes(".")) {
+ const decimalPart = val.split(".")[1];
+ if (decimalPart.length > decimals) {
+ return `Please enter a value with up to ${decimals} decimal places`;
+ }
+ }
+ return true;
+};
diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock
index bedc8090d6..59963bee3a 100644
--- a/apps/mobile/ios/Podfile.lock
+++ b/apps/mobile/ios/Podfile.lock
@@ -2125,6 +2125,49 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
+ - RNSVG (15.11.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNSVG/common (= 15.11.1)
+ - Yoga
+ - RNSVG/common (15.11.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.01.01.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
- SimpleKeychain (1.1.0)
- SocketRocket (0.7.1)
- Yoga (0.0.0)
@@ -2234,6 +2277,7 @@ DEPENDENCIES:
- "RNGoogleSignin (from `../../../node_modules/@react-native-google-signin/google-signin`)"
- RNReanimated (from `../../../node_modules/react-native-reanimated`)
- RNScreens (from `../../../node_modules/react-native-screens`)
+ - RNSVG (from `../../../node_modules/react-native-svg`)
- Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -2454,6 +2498,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-reanimated"
RNScreens:
:path: "../../../node_modules/react-native-screens"
+ RNSVG:
+ :path: "../../../node_modules/react-native-svg"
Yoga:
:path: "../../../node_modules/react-native/ReactCommon/yoga"
@@ -2567,6 +2613,7 @@ SPEC CHECKSUMS:
RNGoogleSignin: edec18754ff4af2cfee46072609ec5a7a754291a
RNReanimated: 5bc01f4a152370c333d50eef11a4169f7db81a91
RNScreens: 27587018b2e6082f5172b1ecf158c14a0e8842d6
+ RNSVG: ea3e35f0375ac20449384fa89ce056ee0e0690ee
SimpleKeychain: f8707c8e97b38c6a6e687b17732afc9bcef06439
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37
diff --git a/apps/mobile/ios/mobile.xcodeproj/project.pbxproj b/apps/mobile/ios/mobile.xcodeproj/project.pbxproj
index 5ecf744720..355fbc296a 100644
--- a/apps/mobile/ios/mobile.xcodeproj/project.pbxproj
+++ b/apps/mobile/ios/mobile.xcodeproj/project.pbxproj
@@ -286,6 +286,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift/ReachabilitySwift.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
@@ -309,6 +310,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ReachabilitySwift.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index bdde89d199..f759088fe9 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -31,9 +31,11 @@
"@react-navigation/native": "^7.0.0",
"@tamagui/babel-plugin": "1.123.0",
"@tamagui/config": "1.123.0",
+ "@tamagui/lucide-icons": "^1.123.14",
"@taquito/utils": "^21.0.0",
"@umami/core": "workspace:^",
"@umami/crypto": "workspace:^",
+ "@umami/data-polling": "workspace:^",
"@umami/multisig": "workspace:^",
"@umami/social-auth": "workspace:^",
"@umami/state": "workspace:^",
@@ -41,7 +43,6 @@
"@umami/tezos": "workspace:^",
"@umami/tzkt": "workspace:^",
"@umami/utils": "workspace:^",
- "@umami/data-polling": "workspace:^",
"@web3auth/base": "^9.5.0",
"@web3auth/base-provider": "^9.5.0",
"@web3auth/react-native-sdk": "^8.1.0",
@@ -70,6 +71,7 @@
"process": "^0.11.10",
"react": "18.3.1",
"react-dom": "18.3.1",
+ "react-hook-form": "^7.54.2",
"react-native": "0.76.3",
"react-native-auth0": "^4.0.0",
"react-native-crypto": "^2.2.0",
@@ -79,6 +81,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
+ "react-native-svg": "^15.11.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.2",
"stream-browserify": "^3.0.0",
diff --git a/apps/mobile/providers/ModalProvider.tsx b/apps/mobile/providers/ModalProvider.tsx
new file mode 100644
index 0000000000..7032081a40
--- /dev/null
+++ b/apps/mobile/providers/ModalProvider.tsx
@@ -0,0 +1,78 @@
+import { type ReactNode, createContext, useContext, useState } from "react";
+import { Dialog } from "tamagui";
+
+type ModalContextType = {
+ showModal: (content: ReactNode) => void;
+ hideModal: () => void;
+};
+
+const ModalContext = createContext(undefined);
+
+export const useModal = () => {
+ const context = useContext(ModalContext);
+ if (!context) {
+ throw new Error("useModal must be used within a ModalProvider");
+ }
+ return context;
+};
+
+type ModalProviderProps = {
+ children: ReactNode;
+};
+
+export const ModalProvider: React.FC = ({ children }) => {
+ const [modalContent, setModalContent] = useState(null);
+ const [isVisible, setIsVisible] = useState(false);
+
+ const showModal = (content: ReactNode) => {
+ setModalContent(content);
+
+ if (!isVisible) {
+ setIsVisible(true);
+ }
+ };
+
+ const hideModal = () => {
+ setIsVisible(false);
+ setTimeout(() => {
+ setModalContent(null);
+ }, 300);
+ };
+
+ return (
+
+ {children}
+
+
+ );
+};
diff --git a/apps/mobile/screens/Home/Home.tsx b/apps/mobile/screens/Home/Home.tsx
index 96cfc59853..ed6a5040c7 100644
--- a/apps/mobile/screens/Home/Home.tsx
+++ b/apps/mobile/screens/Home/Home.tsx
@@ -1,38 +1,34 @@
+import { ArrowDown, ArrowUpRight, Repeat, Wallet } from "@tamagui/lucide-icons";
import { type SocialAccount } from "@umami/core";
-import { useDataPolling } from "@umami/data-polling";
-import {
- useCurrentAccount,
- useGetAccountBalance,
- useGetDollarBalance,
- useSelectNetwork,
- useSelectedNetwork,
-} from "@umami/state";
-import { prettyTezAmount } from "@umami/tezos";
+import { useCurrentAccount, useSelectNetwork, useSelectedNetwork } from "@umami/state";
import { Button, Text, XStack, YStack } from "tamagui";
import { ActionButton, BalanceDisplay, NetworkSwitch } from "./components";
+import { FormPage } from "../../components/SendFlow/Tez";
+import { useModal } from "../../providers/ModalProvider";
import { useSocialOnboarding } from "../../services/auth";
export const Home = () => {
- useDataPolling();
-
const currentAccount = useCurrentAccount();
const network = useSelectedNetwork();
const selectNetwork = useSelectNetwork();
const { logout } = useSocialOnboarding();
+ const { showModal } = useModal();
const address = currentAccount ? currentAccount.address.pkh : "";
- const balance = useGetAccountBalance()(address);
- const balanceInUsd = useGetDollarBalance()(address);
return (
-
+
-
-
-
-
+ } title="Buy" />
+ } title="Swap" />
+ } title="Receive" />
+ }
+ onPress={() => showModal()}
+ title="Send"
+ />
@@ -45,9 +41,7 @@ export const Home = () => {
Current network: {network.name}
Label: {currentAccount?.label}
- Address: {currentAccount?.address.pkh}
- Balance: {prettyTezAmount(balance ?? 0)}
- Balance in USD: {balanceInUsd?.toString() ?? "0"}
+ Address: {address}