diff --git a/.yarn/cache/@expo-cli-npm-0.10.13-9d802519ed-3dda76f39a.zip b/.yarn/cache/@expo-cli-npm-0.10.14-b486c9bd86-88bda58114.zip similarity index 90% rename from .yarn/cache/@expo-cli-npm-0.10.13-9d802519ed-3dda76f39a.zip rename to .yarn/cache/@expo-cli-npm-0.10.14-b486c9bd86-88bda58114.zip index 34a937a1c..9000b06ab 100644 Binary files a/.yarn/cache/@expo-cli-npm-0.10.13-9d802519ed-3dda76f39a.zip and b/.yarn/cache/@expo-cli-npm-0.10.14-b486c9bd86-88bda58114.zip differ diff --git a/.yarn/cache/expo-font-npm-11.6.0-7bada0772e-0260af2045.zip b/.yarn/cache/expo-font-npm-11.6.0-7bada0772e-0260af2045.zip new file mode 100644 index 000000000..5f37c066c Binary files /dev/null and b/.yarn/cache/expo-font-npm-11.6.0-7bada0772e-0260af2045.zip differ diff --git a/.yarn/cache/expo-npm-49.0.14-a27cd66b47-56a1cb91b5.zip b/.yarn/cache/expo-npm-49.0.16-f2d0e3cc5a-5f65fd86a3.zip similarity index 86% rename from .yarn/cache/expo-npm-49.0.14-a27cd66b47-56a1cb91b5.zip rename to .yarn/cache/expo-npm-49.0.16-f2d0e3cc5a-5f65fd86a3.zip index 0f69f0aa0..003b60233 100644 Binary files a/.yarn/cache/expo-npm-49.0.14-a27cd66b47-56a1cb91b5.zip and b/.yarn/cache/expo-npm-49.0.16-f2d0e3cc5a-5f65fd86a3.zip differ diff --git a/app/.expo/types/router.d.ts b/app/.expo/types/router.d.ts deleted file mode 100644 index 8493923e6..000000000 --- a/app/.expo/types/router.d.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable import/export */ -/* eslint-disable @typescript-eslint/ban-types */ -declare module "expo-router" { - import type { LinkProps as OriginalLinkProps } from 'expo-router/build/link/Link'; - import type { Router as OriginalRouter } from 'expo-router/src/types'; - export * from 'expo-router/build'; - - // prettier-ignore - type StaticRoutes = `/` | `/(drawer)/_layout` | `/_layout` | `/(drawer)/contacts/add` | `/contacts/add` | `/(drawer)/contacts/` | `/contacts/` | `/(drawer)/contacts` | `/(drawer)/ledger/link` | `/ledger/link` | `/(drawer)/sessions/` | `/sessions/` | `/(drawer)/sessions` | `/(drawer)/settings/auth` | `/settings/auth` | `/(drawer)/settings/notifications` | `/settings/notifications` | `/(drawer)/token/add` | `/token/add` | `/(drawer)/user` | `/user` | `/_sitemap` | `/accounts/create` | `/accounts/` | `/addresses` | `/confirm` | `/ledger/sign` | `/link/` | `/link/token` | `/onboard/approver` | `/onboard/auth` | `/onboard/` | `/onboard/notifications` | `/onboard/user` | `/scan/`; - // prettier-ignore - type DynamicRoutes = `/(drawer)/${SingleRoutePart}/(home)/_layout` | `/${SingleRoutePart}/_layout` | `/(drawer)/${SingleRoutePart}/(home)/activity` | `/${SingleRoutePart}/activity` | `/(drawer)/${SingleRoutePart}/(home)/` | `/${SingleRoutePart}/` | `/(drawer)/${SingleRoutePart}/(home)` | `/(drawer)/${SingleRoutePart}/policies/${SingleRoutePart}/${SingleRoutePart}/` | `/${SingleRoutePart}/policies/${SingleRoutePart}/${SingleRoutePart}/` | `/(drawer)/${SingleRoutePart}/policies/${SingleRoutePart}/${SingleRoutePart}` | `/(drawer)/${SingleRoutePart}/policies/${SingleRoutePart}/approvers` | `/${SingleRoutePart}/policies/${SingleRoutePart}/approvers` | `/(drawer)/${SingleRoutePart}/policies/${SingleRoutePart}/` | `/${SingleRoutePart}/policies/${SingleRoutePart}/` | `/(drawer)/${SingleRoutePart}/policies/${SingleRoutePart}` | `/(drawer)/${SingleRoutePart}/policies/` | `/${SingleRoutePart}/policies/` | `/(drawer)/${SingleRoutePart}/policies` | `/(drawer)/${SingleRoutePart}/swap` | `/${SingleRoutePart}/swap` | `/(drawer)/${SingleRoutePart}/tokens` | `/${SingleRoutePart}/tokens` | `/(drawer)/${SingleRoutePart}/transfer` | `/${SingleRoutePart}/transfer` | `/(drawer)/approver/${SingleRoutePart}/` | `/approver/${SingleRoutePart}/` | `/(drawer)/approver/${SingleRoutePart}` | `/(drawer)/contacts/${SingleRoutePart}` | `/contacts/${SingleRoutePart}` | `/(drawer)/message/${SingleRoutePart}/_layout` | `/message/${SingleRoutePart}/_layout` | `/(drawer)/message/${SingleRoutePart}/` | `/message/${SingleRoutePart}/` | `/(drawer)/message/${SingleRoutePart}` | `/(drawer)/message/${SingleRoutePart}/policy` | `/message/${SingleRoutePart}/policy` | `/(drawer)/token/${SingleRoutePart}` | `/token/${SingleRoutePart}` | `/(drawer)/transaction/${SingleRoutePart}/_layout` | `/transaction/${SingleRoutePart}/_layout` | `/(drawer)/transaction/${SingleRoutePart}/` | `/transaction/${SingleRoutePart}/` | `/(drawer)/transaction/${SingleRoutePart}` | `/(drawer)/transaction/${SingleRoutePart}/policy` | `/transaction/${SingleRoutePart}/policy` | `/(drawer)/transaction/${SingleRoutePart}/transaction` | `/transaction/${SingleRoutePart}/transaction` | `/${CatchAllRoutePart}` | `/${SingleRoutePart}/name` | `/${SingleRoutePart}/policies/${SingleRoutePart}/${SingleRoutePart}/add-selector` | `/${SingleRoutePart}/policies/${SingleRoutePart}/name` | `/${SingleRoutePart}/policies/template` | `/${SingleRoutePart}/receive` | `/approver/${SingleRoutePart}/qr` | `/scan/${SingleRoutePart}` | `/sessions/${SingleRoutePart}` | `/sessions/connect/${SingleRoutePart}`; - // prettier-ignore - type DynamicRouteTemplate = `/(drawer)/[account]/(home)/_layout` | `/(drawer)/[account]/(home)/activity` | `/(drawer)/[account]/(home)/` | `/(drawer)/[account]/policies/[key]/[contract]/` | `/(drawer)/[account]/policies/[key]/approvers` | `/(drawer)/[account]/policies/[key]/` | `/(drawer)/[account]/policies/` | `/(drawer)/[account]/swap` | `/(drawer)/[account]/tokens` | `/(drawer)/[account]/transfer` | `/(drawer)/approver/[address]/` | `/(drawer)/contacts/[address]` | `/(drawer)/message/[hash]/_layout` | `/(drawer)/message/[hash]/` | `/(drawer)/message/[hash]/policy` | `/(drawer)/token/[token]` | `/(drawer)/transaction/[hash]/_layout` | `/(drawer)/transaction/[hash]/` | `/(drawer)/transaction/[hash]/policy` | `/(drawer)/transaction/[hash]/transaction` | `/[...unmatched]` | `/[account]/name` | `/[account]/policies/[key]/[contract]/add-selector` | `/[account]/policies/[key]/name` | `/[account]/policies/template` | `/[account]/receive` | `/approver/[address]/qr` | `/scan/[address]` | `/sessions/[topic]` | `/sessions/connect/[id]`; - - type RelativePathString = `./${string}` | `../${string}` | '..'; - type AbsoluteRoute = DynamicRouteTemplate | StaticRoutes; - type ExternalPathString = `http${string}`; - type ExpoRouterRoutes = DynamicRouteTemplate | StaticRoutes | RelativePathString; - type AllRoutes = ExpoRouterRoutes | ExternalPathString; - - /**************** - * Route Utils * - ****************/ - - type SearchOrHash = `?${string}` | `#${string}`; - type UnknownInputParams = Record; - type UnknownOutputParams = Record; - - /** - * Return only the RoutePart of a string. If the string has multiple parts return never - * - * string | type - * ---------|------ - * 123 | 123 - * /123/abc | never - * 123?abc | never - * ./123 | never - * /123 | never - * 123/../ | never - */ - type SingleRoutePart = S extends `${string}/${string}` - ? never - : S extends `${string}${SearchOrHash}` - ? never - : S extends '' - ? never - : S extends `(${string})` - ? never - : S extends `[${string}]` - ? never - : S; - - /** - * Return only the CatchAll router part. If the string has search parameters or a hash return never - */ - type CatchAllRoutePart = S extends `${string}${SearchOrHash}` - ? never - : S extends '' - ? never - : S extends `${string}(${string})${string}` - ? never - : S extends `${string}[${string}]${string}` - ? never - : S; - - // type OptionalCatchAllRoutePart = S extends `${string}${SearchOrHash}` ? never : S - - /** - * Return the name of a route parameter - * '[test]' -> 'test' - * 'test' -> never - * '[...test]' -> '...test' - */ - type IsParameter = Part extends `[${infer ParamName}]` ? ParamName : never; - - /** - * Return a union of all parameter names. If there are no names return never - * - * /[test] -> 'test' - * /[abc]/[...def] -> 'abc'|'...def' - */ - type ParameterNames = Path extends `${infer PartA}/${infer PartB}` - ? IsParameter | ParameterNames - : IsParameter; - - /** - * Returns all segements of a route. - * - * /(group)/123/abc/[id]/[...rest] -> ['(group)', '123', 'abc', '[id]', '[...rest]' - */ - type RouteSegments = Path extends `${infer PartA}/${infer PartB}` - ? PartA extends '' | '.' - ? [...RouteSegments] - : [PartA, ...RouteSegments] - : Path extends '' - ? [] - : [Path]; - - /** - * Returns a Record of the routes parameters as strings and CatchAll parameters - * - * There are two versions, input and output, as you can input 'string | number' but - * the output will always be 'string' - * - * /[id]/[...rest] -> { id: string, rest: string[] } - * /no-params -> {} - */ - type InputRouteParams = { - [Key in ParameterNames as Key extends `...${infer Name}` - ? Name - : Key]: Key extends `...${string}` ? (string | number)[] : string | number; - } & UnknownInputParams; - - type OutputRouteParams = { - [Key in ParameterNames as Key extends `...${infer Name}` - ? Name - : Key]: Key extends `...${string}` ? string[] : string; - } & UnknownOutputParams; - - /** - * Returns the search parameters for a route. - */ - export type SearchParams = T extends DynamicRouteTemplate - ? OutputRouteParams - : T extends StaticRoutes - ? never - : UnknownOutputParams; - - /** - * Route is mostly used as part of Href to ensure that a valid route is provided - * - * Given a dynamic route, this will return never. This is helpful for conditional logic - * - * /test -> /test, /test2, etc - * /test/[abc] -> never - * /test/resolve -> /test, /test2, etc - * - * Note that if we provide a value for [abc] then the route is allowed - * - * This is named Route to prevent confusion, as users they will often see it in tooltips - */ - export type Route = T extends string - ? T extends DynamicRouteTemplate - ? never - : - | StaticRoutes - | RelativePathString - | ExternalPathString - | (T extends `${infer P}${SearchOrHash}` - ? P extends DynamicRoutes - ? T - : never - : T extends DynamicRoutes - ? T - : never) - : never; - - /********* - * Href * - *********/ - - export type Href = T extends Record<'pathname', string> ? HrefObject : Route; - - export type HrefObject< - R extends Record<'pathname', string>, - P = R['pathname'] - > = P extends DynamicRouteTemplate - ? { pathname: P; params: InputRouteParams

} - : P extends Route

- ? { pathname: Route

| DynamicRouteTemplate; params?: never | InputRouteParams } - : never; - - /*********************** - * Expo Router Exports * - ***********************/ - - export type Router = Omit & { - /** Navigate to the provided href. */ - push: (href: Href) => void; - /** Navigate to route without appending to the history. */ - replace: (href: Href) => void; - /** Update the current route query params. */ - setParams: (params?: T extends '' ? Record : InputRouteParams) => void; - }; - - /** The imperative router. */ - export const router: Router; - - /************ - * * - ************/ - export interface LinkProps extends OriginalLinkProps { - href: Href; - } - - export interface LinkComponent { - (props: React.PropsWithChildren>): JSX.Element; - /** Helper method to resolve an Href object into a string. */ - resolveHref: (href: Href) => string; - } - - /** - * Component to render link to another route using a path. - * Uses an anchor tag on the web. - * - * @param props.href Absolute path to route (e.g. `/feeds/hot`). - * @param props.replace Should replace the current route without adding to the history. - * @param props.asChild Forward props to child component. Useful for custom buttons. - * @param props.children Child elements to render the content. - */ - export const Link: LinkComponent; - - /** Redirects to the href as soon as the component is mounted. */ - export const Redirect: ( - props: React.PropsWithChildren<{ href: Href }> - ) => JSX.Element; - - /************ - * Hooks * - ************/ - export function useRouter(): Router; - - export function useLocalSearchParams< - T extends AllRoutes | UnknownOutputParams = UnknownOutputParams - >(): T extends AllRoutes ? SearchParams : T; - - /** @deprecated renamed to `useGlobalSearchParams` */ - export function useSearchParams< - T extends AllRoutes | UnknownOutputParams = UnknownOutputParams - >(): T extends AllRoutes ? SearchParams : T; - - export function useGlobalSearchParams< - T extends AllRoutes | UnknownOutputParams = UnknownOutputParams - >(): T extends AllRoutes ? SearchParams : T; - - export function useSegments< - T extends AbsoluteRoute | RouteSegments | RelativePathString - >(): T extends AbsoluteRoute ? RouteSegments : T extends string ? string[] : T; -} diff --git a/app/.gitignore b/app/.gitignore index 9b21c78b2..c01463acb 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,5 +1,4 @@ .expo/* -!.expo/types .vscode/.react web-build **/*generated.ts diff --git a/app/assets/app-store-badge.svg b/app/assets/app-store-badge.svg new file mode 100755 index 000000000..40efc977f --- /dev/null +++ b/app/assets/app-store-badge.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/fonts/Roboto-400-Regular.ttf b/app/assets/fonts/Roboto-400-Regular.ttf new file mode 100644 index 000000000..ddf4bfacb Binary files /dev/null and b/app/assets/fonts/Roboto-400-Regular.ttf differ diff --git a/app/assets/fonts/Roboto-500-Medium.ttf b/app/assets/fonts/Roboto-500-Medium.ttf new file mode 100644 index 000000000..ac0f908b9 Binary files /dev/null and b/app/assets/fonts/Roboto-500-Medium.ttf differ diff --git a/app/assets/google-play-badge.png b/app/assets/google-play-badge.png new file mode 100644 index 000000000..4acc1a383 Binary files /dev/null and b/app/assets/google-play-badge.png differ diff --git a/app/assets/logo-color.svg b/app/assets/logo-color.svg deleted file mode 100644 index 10997a7f8..000000000 --- a/app/assets/logo-color.svg +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/assets/logo.svg b/app/assets/logo.svg index 3b6efc30f..25046ba25 100644 --- a/app/assets/logo.svg +++ b/app/assets/logo.svg @@ -1,3 +1,49 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/screenshots.png b/app/assets/screenshots.png new file mode 100644 index 000000000..811dc2134 Binary files /dev/null and b/app/assets/screenshots.png differ diff --git a/app/assets/twitter-color.svg b/app/assets/twitter-color.svg new file mode 100644 index 000000000..abbf45576 --- /dev/null +++ b/app/assets/twitter-color.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/app/assets/twitter.svg b/app/assets/twitter.svg index ddd89cee0..0704621d5 100644 --- a/app/assets/twitter.svg +++ b/app/assets/twitter.svg @@ -1,20 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/app/package.json b/app/package.json index e524b39b3..3603614d4 100644 --- a/app/package.json +++ b/app/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "main": "index.js", "scripts": { - "prebuild": "(cd .. && yarn lib build) && yarn generate:gql", + "prebuild": "(cd .. && yarn lib build) && yarn generate:gql && yarn expo customize tsconfig.json", "start": "APP_VARIANT=dev yarn expo start --dev-client", "test": "yarn typecheck && jest", "typecheck": "tsc --noEmit", @@ -69,7 +69,7 @@ "eas-cli": "^5.4.0", "eth-url-parser": "^1.0.4", "ethers": "^5.7.2", - "expo": "^49.0.13", + "expo": "^49.0.16", "expo-apple-authentication": "~6.1.0", "expo-application": "~5.3.0", "expo-asset": "~8.10.1", @@ -81,6 +81,7 @@ "expo-constants": "~14.4.2", "expo-dev-client": "~2.4.11", "expo-device": "~5.4.0", + "expo-font": "~11.6.0", "expo-haptics": "~12.4.0", "expo-image": "~1.3.4", "expo-keep-awake": "~12.3.0", diff --git a/app/src/app/(drawer)/[account]/(home)/_layout.tsx b/app/src/app/(drawer)/[account]/(home)/_layout.tsx index 3f9b5c716..a665a29cb 100644 --- a/app/src/app/(drawer)/[account]/(home)/_layout.tsx +++ b/app/src/app/(drawer)/[account]/(home)/_layout.tsx @@ -1,16 +1,18 @@ import { useEffect } from 'react'; -import { StyleSheet, View } from 'react-native'; import { z } from 'zod'; import { TopTabs } from '~/components/layout/TopTabs'; import { HomeHeader } from '~/components/home/HomeHeader'; import { useLocalParams } from '~/hooks/useLocalParams'; -import { useSetSelectedAccont } from '~/hooks/useSelectedAccount'; +import { useSelectedAccount, useSetSelectedAccont } from '~/hooks/useSelectedAccount'; import { zAddress } from '~/lib/zod'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; -const HomeLayoutParams = z.object({ account: zAddress }); +const HomeLayoutParams = z.object({ account: zAddress.optional() }); export default function HomeLayout() { - const { account } = useLocalParams(`/(drawer)/[account]/(home)/_layout`, HomeLayoutParams); + const lastSelected = useSelectedAccount(); + const account = + useLocalParams(`/(drawer)/[account]/(home)/_layout`, HomeLayoutParams).account ?? lastSelected!; const setSelectedAccount = useSetSelectedAccont(); useEffect(() => { @@ -18,7 +20,7 @@ export default function HomeLayout() { }, [account]); return ( - + @@ -29,12 +31,6 @@ export default function HomeLayout() { initialParams={{ account }} /> - + ); } - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, -}); diff --git a/app/src/app/(drawer)/[account]/policies/[key]/[contract]/index.tsx b/app/src/app/(drawer)/[account]/policies/[key]/[contract]/index.tsx index cf7a21583..a734e061d 100644 --- a/app/src/app/(drawer)/[account]/policies/[key]/[contract]/index.tsx +++ b/app/src/app/(drawer)/[account]/policies/[key]/[contract]/index.tsx @@ -1,4 +1,3 @@ -import { useRouter } from 'expo-router'; import { useImmerAtom } from 'jotai-immer'; import { ERC20_ABI, @@ -27,6 +26,8 @@ import { zAddress } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; +import { useGetSelector } from '~/app/selector'; const Query = gql(/* GraphQL */ ` query ContractPermissionsScreen($contract: Address!) { @@ -53,20 +54,21 @@ const ERC20_FUNCTIONS = ERC20_ABI.filter( abi, })); -export const ContractPermissionsScheme = z.object({ +export const ContractPermissionsParams = z.object({ account: zAddress, key: z.string(), contract: zAddress, }); +export type ContractPermissionsParams = z.infer; function ContractPermissionsScreen() { const params = useLocalParams( `/(drawer)/[account]/policies/[key]/[contract]/`, - ContractPermissionsScheme, + ContractPermissionsParams, ); - const address = params.account; - const router = useRouter(); + const address = params.contract; const { contract, token } = useQuery(Query, { contract: address }).data; + const getSelector = useGetSelector(); const [{ permissions }, updatePolicy] = useImmerAtom(POLICY_DRAFT_ATOM); @@ -89,66 +91,82 @@ function ContractPermissionsScreen() { ); return ( - + <> ( - router.push({ pathname: `/[account]/policies/[key]/[contract]/add-selector`, params }) - } - /> - )} - /> + onPress={async () => { + const selector = await getSelector(); + if (!selector) return; - {token && } - - Actions - - updatePolicy((draft) => { - draft.permissions.targets.contracts[address] = { - functions: draft.permissions.targets.contracts[address]?.functions ?? {}, - defaultAllow: enabled, + draft.permissions.targets.contracts[address] ??= { + defaultAllow: draft.permissions.targets.default.defaultAllow, + functions: {}, }; - }) - } + draft.permissions.targets.contracts[address].functions[selector] = true; + }); + }} /> - } + )} /> - {functions.map((f) => { - if (SPENDING_TRANSFER_FUNCTIONS.has(f.selector)) return null; // Handled by SpendingLimit + + + {token && } + + Actions - return ( updatePolicy((draft) => { - setTargetAllowed(draft.permissions.targets, address, f.selector, enabled); + draft.permissions.targets.contracts[address] = { + functions: draft.permissions.targets.contracts[address]?.functions ?? {}, + defaultAllow: enabled, + }; }) } /> } /> - ); - })} - + + {functions.map((f) => { + if (SPENDING_TRANSFER_FUNCTIONS.has(f.selector)) return null; // Handled by SpendingLimit + + return ( + + updatePolicy((draft) => { + setTargetAllowed(draft.permissions.targets, address, f.selector, enabled); + }) + } + /> + } + /> + ); + })} + + + ); } const styles = StyleSheet.create({ + surface: { + paddingTop: 8, + }, container: { flexGrow: 1, }, diff --git a/app/src/app/(drawer)/[account]/policies/[key]/approvers.tsx b/app/src/app/(drawer)/[account]/policies/[key]/approvers.tsx index 6b670c2e5..a726eb2ce 100644 --- a/app/src/app/(drawer)/[account]/policies/[key]/approvers.tsx +++ b/app/src/app/(drawer)/[account]/policies/[key]/approvers.tsx @@ -16,6 +16,7 @@ import { useSelectAddress } from '~/hooks/useSelectAddress'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; export type PolicyApproversScreenRoute = `/(drawer)/[account]/policies/[key]/approvers`; export type PolicyApproversScreenParams = SearchParams; @@ -59,36 +60,37 @@ function PolicyApproversScreen() { }; return ( - + <> - - - + + + + - remove(item)} />} - keyExtractor={(item) => item} - estimatedItemSize={ListItemHeight.SINGLE_LINE} - ListEmptyComponent={ - - No approvals are required - literally anyone may execute a transaction using this - policy. Make sure this is intended! - - } - /> + remove(item)} />} + keyExtractor={(item) => item} + estimatedItemSize={ListItemHeight.SINGLE_LINE} + ListEmptyComponent={ + + No approvals are required - literally anyone may execute a transaction using this + policy. Make sure this is intended! + + } + /> - { - addApprover(); - console.log('pressed'); - }} - variant="primary" - /> - + { + addApprover(); + }} + variant="primary" + /> + + ); } @@ -99,8 +101,7 @@ const useStyles = makeStyles(({ colors }) => ({ chipContainer: { flexDirection: 'row', justifyContent: 'center', - marginTop: 8, - marginBottom: 16, + marginVertical: 16, }, noApproversText: { textAlign: 'center', diff --git a/app/src/app/(drawer)/[account]/policies/[key]/index.tsx b/app/src/app/(drawer)/[account]/policies/[key]/index.tsx index 03133a020..4ee5de380 100644 --- a/app/src/app/(drawer)/[account]/policies/[key]/index.tsx +++ b/app/src/app/(drawer)/[account]/policies/[key]/index.tsx @@ -19,6 +19,7 @@ import { POLICY_DRAFT_ATOM, asPolicyInput } from '~/lib/policy/draft'; import { showError } from '~/components/provider/SnackbarProvider'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query PolicyScreen($account: Address!, $key: PolicyKey!, $queryPolicy: Boolean!) { @@ -116,7 +117,7 @@ function PolicyScreen() { if (!account) return null; return ( - + <> setDraft(init) : undefined} /> - - router.push({ - pathname: `/(drawer)/[account]/policies/[key]/approvers`, - params: { account: draft.account, key: draft.key ?? 'add' }, - }) - } - /> - - - - {(draft.key === undefined || isModified) && ( - { - const input = { ...asPolicyInput(draft), account: draft.account }; - const r = - input.key !== undefined - ? (await update({ input })).data?.updatePolicy - : (await create({ input })).data?.createPolicy; - - const proposal = r?.draft?.proposal; - if (!proposal) return showError('Failed to propose changes'); - - router.setParams({ ...params, key: `${r.key}`, view: 'draft' }); - router.push({ - pathname: `/(drawer)/transaction/[hash]/`, - params: { hash: proposal.hash }, - }); - }} - /> - )} - + + + + router.push({ + pathname: `/(drawer)/[account]/policies/[key]/approvers`, + params: { account: draft.account, key: draft.key ?? 'add' }, + }) + } + /> + + + + {(draft.key === undefined || isModified) && ( + { + const input = { ...asPolicyInput(draft), account: draft.account }; + const r = + input.key !== undefined + ? (await update({ input })).data?.updatePolicy + : (await create({ input })).data?.createPolicy; + + const proposal = r?.draft?.proposal; + if (!proposal) return showError('Failed to propose changes'); + + router.setParams({ ...params, key: `${r.key}`, view: 'draft' }); + router.push({ + pathname: `/(drawer)/transaction/[hash]/`, + params: { hash: proposal.hash }, + }); + }} + /> + )} + + + ); } diff --git a/app/src/app/(drawer)/[account]/policies/index.tsx b/app/src/app/(drawer)/[account]/policies/index.tsx index 8d664cf80..36fa07fe1 100644 --- a/app/src/app/(drawer)/[account]/policies/index.tsx +++ b/app/src/app/(drawer)/[account]/policies/index.tsx @@ -2,7 +2,7 @@ import { useRouter } from 'expo-router'; import { gql } from '@api/generated'; import { FlashList } from '@shopify/flash-list'; import { EditIcon, NavigateNextIcon, PlusIcon } from '@theme/icons'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet } from 'react-native'; import { Menu } from 'react-native-paper'; import { AppbarMore } from '~/components/Appbar/AppbarMore'; import { NotFound } from '~/components/NotFound'; @@ -17,6 +17,7 @@ import { zAddress } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query PoliciesScreen($account: Address!) { @@ -45,7 +46,7 @@ function PoliciesScreen() { if (!account) return ; return ( - + <> - Security Policies} - renderItem={({ item: policy }) => ( - { - router.push({ - pathname: `/(drawer)/[account]/policies/[key]/`, - params: { account: account.address, key: policy.key }, - }); - }} - /> - )} - estimatedItemSize={ListItemHeight.DOUBLE_LINE} - keyExtractor={(item) => item.id} - contentContainerStyle={styles.contentContainer} - showsVerticalScrollIndicator={false} - /> + + Security Policies} + renderItem={({ item: policy }) => ( + { + router.push({ + pathname: `/(drawer)/[account]/policies/[key]/`, + params: { account: account.address, key: policy.key }, + }); + }} + /> + )} + estimatedItemSize={ListItemHeight.DOUBLE_LINE} + keyExtractor={(item) => item.id} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + /> - - router.push({ - pathname: `/[account]/policies/template`, - params: { account: account.address }, - }) - } - /> - + + router.push({ + pathname: `/[account]/policies/template`, + params: { account: account.address }, + }) + } + /> + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - }, contentContainer: { paddingVertical: 8, }, diff --git a/app/src/app/(drawer)/[account]/swap.tsx b/app/src/app/(drawer)/[account]/swap.tsx index 10bfd9701..753711323 100644 --- a/app/src/app/(drawer)/[account]/swap.tsx +++ b/app/src/app/(drawer)/[account]/swap.tsx @@ -22,6 +22,7 @@ import { zAddress } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query SwapScreen($account: Address!, $from: Address!, $to: Address!, $skipTo: Boolean!) { @@ -86,64 +87,63 @@ function SwapScreen() { : fiatToToken(parseFloat(fromInput), from.price?.current ?? 0, from.decimals); return ( - + <> - - - - - - - - - - - + label: `Swap ${from.symbol} for ${to!.symbol}`, + operations: await getSwapOperations({ + account, + pool: pool!, + from: { + token: fromAddress, + amount: fromAmount, + }, + slippage: 0.01, // 1% + deadline: DateTime.now().plus({ months: 3 }), + }), + }); + router.push({ pathname: `/(drawer)/transaction/[hash]/`, params: { hash: proposal } }); + }} + > + Propose + + + ); } const useStyles = makeStyles(() => ({ - root: { - flex: 1, - }, spacer: { flex: 1, }, diff --git a/app/src/app/(drawer)/[account]/tokens.tsx b/app/src/app/(drawer)/[account]/tokens.tsx index 1e50c3533..13f097571 100644 --- a/app/src/app/(drawer)/[account]/tokens.tsx +++ b/app/src/app/(drawer)/[account]/tokens.tsx @@ -1,7 +1,7 @@ -import { useRouter } from 'expo-router'; +import { Stack, useRouter } from 'expo-router'; import { StyleSheet, View } from 'react-native'; import { Address } from 'lib'; -import { Searchbar } from '~/components/fields/Searchbar'; +import { Searchbar } from '~/components/Appbar/Searchbar'; import { AddIcon, SearchIcon } from '@theme/icons'; import { ListHeader } from '~/components/list/ListHeader'; import { TokenItem } from '~/components/token/TokenItem'; @@ -19,6 +19,10 @@ import { zAddress, zArray } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { SearchbarOptions } from '~/components/Appbar/SearchbarOptions'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; +import { Drawer } from '~/components/drawer/Drawer'; +import { SearchbarHeader } from '~/components/Appbar/SearchbarHeader'; const Query = gql(/* GraphQL */ ` query TokensScreen($account: Address!, $query: String, $feeToken: Boolean) { @@ -74,44 +78,43 @@ function TokensScreen() { ).data.tokens ?? []; return ( - - + } placeholder="Search tokens" - trailing={[ - SearchIcon, - (props) => router.push(`/token/add`)} />, - ]} + trailing={(props) => router.push(`/token/add`)} />} value={query} onChangeText={setQuery} /> - Tokens} - renderItem={({ item: token }) => ( - { - if (TOKEN_SELECTED.observed) { - TOKEN_SELECTED.next(token.address); - } else { - router.push({ - pathname: `/(drawer)/token/[token]`, - params: { token: token.address }, - }); - } - }} - disabled={disabled?.has(token.address) || (enabled && !enabled.has(token.address))} - /> - )} - contentContainerStyle={styles.container} - showsVerticalScrollIndicator={false} - estimatedItemSize={ListItemHeight.DOUBLE_LINE} - keyExtractor={(item) => item.id} - /> - + + Tokens} + renderItem={({ item: token }) => ( + { + if (TOKEN_SELECTED.observed) { + TOKEN_SELECTED.next(token.address); + } else { + router.push({ + pathname: `/(drawer)/token/[token]`, + params: { token: token.address }, + }); + } + }} + disabled={disabled?.has(token.address) || (enabled && !enabled.has(token.address))} + /> + )} + contentContainerStyle={styles.container} + showsVerticalScrollIndicator={false} + estimatedItemSize={ListItemHeight.DOUBLE_LINE} + keyExtractor={(item) => item.id} + /> + + ); } diff --git a/app/src/app/(drawer)/[account]/transfer.tsx b/app/src/app/(drawer)/[account]/transfer.tsx index f307a1791..3f3dd6a20 100644 --- a/app/src/app/(drawer)/[account]/transfer.tsx +++ b/app/src/app/(drawer)/[account]/transfer.tsx @@ -22,6 +22,7 @@ import { useLocalParams } from '~/hooks/useLocalParams'; import { Actions } from '~/components/layout/Actions'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query TransferScreen($account: Address!, $token: Address!) { @@ -72,48 +73,50 @@ function TransferScreen() { : fiatToToken(parseFloat(inputAmount), token.price?.current ?? 0, token.decimals); return ( - + <> - - - - - { - const token = await selectToken({ account }); - if (token) setToken(token); - }} - /> - - - - - - - - + /> + + + + + + + + + ); } diff --git a/app/src/app/(drawer)/_layout.tsx b/app/src/app/(drawer)/_layout.tsx index 7d234ed74..adb410d81 100644 --- a/app/src/app/(drawer)/_layout.tsx +++ b/app/src/app/(drawer)/_layout.tsx @@ -1,9 +1,10 @@ import { Drawer } from '~/components/drawer/Drawer'; +import { RootDrawer } from '~/components/drawer/RootDrawer'; export const unstable_settings = { initialRouteName: `[account]/(home)`, }; export default function DrawerLayout() { - return ; + return ; } diff --git a/app/src/app/(drawer)/approver/[address]/index.tsx b/app/src/app/(drawer)/approver/[address]/index.tsx index 99587e088..20faca832 100644 --- a/app/src/app/(drawer)/approver/[address]/index.tsx +++ b/app/src/app/(drawer)/approver/[address]/index.tsx @@ -16,6 +16,7 @@ import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { getDeviceModel } from '~/lib/device'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query ApproverDetails($approver: Address) { @@ -70,49 +71,49 @@ function ApproverScreen() { const takenNames = user.approvers.filter((a) => a.id !== approver.id).map((a) => a.name); return ( - + <> - - : undefined} - label="Label" - placeholder="iPhone" - containerStyle={styles.inset} - rules={{ - required: true, - validate: (v) => !takenNames.includes(v) || 'An approver with ths name already exists', - }} - onBlur={handleSubmit(async ({ name }) => { - await update({ approver: approver.address, name }); - })} - /> - + + + : undefined} + label="Label" + placeholder="iPhone" + containerStyle={styles.inset} + rules={{ + required: true, + validate: (v) => + !takenNames.includes(v) || 'An approver with ths name already exists', + }} + onBlur={handleSubmit(async ({ name }) => { + await update({ approver: approver.address, name }); + })} + /> + - - - - + + + + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, - }, fields: { marginVertical: 16, gap: 16, diff --git a/app/src/app/(drawer)/contacts/index.tsx b/app/src/app/(drawer)/contacts/index.tsx index aff2f02ac..8030d7400 100644 --- a/app/src/app/(drawer)/contacts/index.tsx +++ b/app/src/app/(drawer)/contacts/index.tsx @@ -3,12 +3,12 @@ import { useState } from 'react'; import { View } from 'react-native'; import { NavigateNextIcon, ScanIcon, SearchIcon, materialCommunityIcon } from '~/util/theme/icons'; import { Address } from 'lib'; -import { Searchbar } from '~/components/fields/Searchbar'; +import { Searchbar } from '~/components/Appbar/Searchbar'; import { ListHeader } from '~/components/list/ListHeader'; import { ListItemHeight } from '~/components/list/ListItem'; import { gql } from '@api/generated'; import { FlashList } from '@shopify/flash-list'; -import { Text } from 'react-native-paper'; +import { Text, Surface } from 'react-native-paper'; import { Fab } from '~/components/Fab'; import { makeStyles } from '@theme/makeStyles'; import { useQuery } from '~/gql'; @@ -17,6 +17,7 @@ import { ContactItem } from '~/components/item/ContactItem'; import { AppbarMenu } from '~/components/Appbar/AppbarMenu'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query ContactsScreen($query: String) { @@ -47,68 +48,64 @@ function ContactsScreen() { const { contacts } = useQuery(Query, { query }).data; return ( - + <> } placeholder="Search contacts" - trailing={[ - SearchIcon, - (props) => ( - { - const address = await scanAddress(); - if (address) { - router.push({ - pathname: `/(drawer)/contacts/[address]`, - params: { address }, - }); - } - }} - /> - ), - ]} + trailing={(props) => ( + { + const address = await scanAddress(); + if (address) { + router.push({ + pathname: `/(drawer)/contacts/[address]`, + params: { address }, + }); + } + }} + /> + )} value={query} onChangeText={setQuery} /> - Contacts} - renderItem={({ item }) => ( - - router.push({ - pathname: `/(drawer)/contacts/[address]`, - params: { address: item.address }, - }) - } - /> - )} - ListEmptyComponent={ - - Add a contact to get started - - } - extraData={[disabled, router.push]} - contentContainerStyle={styles.contentContainer} - showsVerticalScrollIndicator={false} - estimatedItemSize={ListItemHeight.DOUBLE_LINE} - /> + + Contacts} + renderItem={({ item }) => ( + + router.push({ + pathname: `/(drawer)/contacts/[address]`, + params: { address: item.address }, + }) + } + /> + )} + ListEmptyComponent={ + + Add a contact to get started + + } + extraData={[disabled, router.push]} + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + estimatedItemSize={ListItemHeight.DOUBLE_LINE} + /> - router.push(`/contacts/add`)} /> - + router.push(`/contacts/add`)} /> + + ); } -const useStyles = makeStyles(({ colors }) => ({ - container: { - flex: 1, - }, +const useStyles = makeStyles(({ colors, corner }) => ({ contentContainer: { paddingVertical: 8, }, diff --git a/app/src/app/(drawer)/ledger/link.tsx b/app/src/app/(drawer)/ledger/link.tsx index 3b4042d72..9759b4828 100644 --- a/app/src/app/(drawer)/ledger/link.tsx +++ b/app/src/app/(drawer)/ledger/link.tsx @@ -16,6 +16,7 @@ import useBluetoothPermissions from '~/hooks/ble/useBluetoothPermissions'; import { useMemo } from 'react'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query LinkLedgerScreen { @@ -34,65 +35,64 @@ function LinkLedgerScreen() { const { user } = useQuery(Query).data; return ( - + <> - - + + + - - Unlock your Ledger, enable bluetooth, and open the Ethereum app - - + + Unlock your Ledger, enable bluetooth, and open the Ethereum app + + - {devices.isOk() ? ( - }> - Available devices - - } - renderItem={({ item }) => } - extraData={[user]} - contentContainerStyle={styles.listContainer} - keyExtractor={(d) => d.id} - /> - ) : ( - match(devices.error) - .with('permissions-required', () => ( - <> + {devices.isOk() ? ( + }> + Available devices + + } + renderItem={({ item }) => } + extraData={[user]} + contentContainerStyle={styles.listContainer} + keyExtractor={(d) => d.id} + /> + ) : ( + match(devices.error) + .with('permissions-required', () => ( + <> + + Please grant bluetooth related permissions in order to scan and connect + + + + + + + )) + .with('disabled', () => ( - Please grant bluetooth related permissions in order to scan and connect + Please enable bluetooth - - - - - - )) - .with('disabled', () => ( - - Please enable bluetooth - - )) - .with('unsupported', () => ( - - Bluetooth is unsupported - - )) - .exhaustive() - )} - + )) + .with('unsupported', () => ( + + Bluetooth is unsupported + + )) + .exhaustive() + )} + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, - }, headerContainer: { marginHorizontal: 16, marginVertical: 32, diff --git a/app/src/app/(drawer)/message/[hash]/_layout.tsx b/app/src/app/(drawer)/message/[hash]/_layout.tsx index 1c6cfdae5..c4202b363 100644 --- a/app/src/app/(drawer)/message/[hash]/_layout.tsx +++ b/app/src/app/(drawer)/message/[hash]/_layout.tsx @@ -12,7 +12,8 @@ import { useRouter } from 'expo-router'; import { TopTabs } from '~/components/layout/TopTabs'; import { MessageProposalActions } from '~/components/proposal/MessageProposalActions'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet, ScrollView } from 'react-native'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query MessageLayout($proposal: Bytes32!) { @@ -56,7 +57,7 @@ export default function MessageLayout() { if (!proposal) return query.stale ? null : ; return ( - + <> ( @@ -77,26 +78,30 @@ export default function MessageLayout() { )} /> - - - - + + + + + + - - + + + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, + container: { + flexGrow: 1, }, }); diff --git a/app/src/app/(drawer)/sessions/index.tsx b/app/src/app/(drawer)/sessions/index.tsx index 5d9225795..5f49548a9 100644 --- a/app/src/app/(drawer)/sessions/index.tsx +++ b/app/src/app/(drawer)/sessions/index.tsx @@ -1,14 +1,14 @@ import { useRouter } from 'expo-router'; import { ScanIcon } from '@theme/icons'; -import { StyleSheet, View } from 'react-native'; +import { StyleSheet, FlatList } from 'react-native'; import { useWalletConnect } from '~/util/walletconnect'; -import { FlashList } from '@shopify/flash-list'; import { PairingItem } from '~/components/walletconnect/PairingItem'; -import { ListItemHeight } from '~/components/list/ListItem'; import { Divider, Text } from 'react-native-paper'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; +import { Fab } from '~/components/Fab'; function SessionsScreen() { const router = useRouter(); @@ -17,49 +17,39 @@ function SessionsScreen() { const pairings = client.pairing.values; return ( - - router.push(`/scan/`)} />} - /> + <> + - ( - <> - - {index < pairings.length - 1 && } - - )} - ListEmptyComponent={ - - No active sessions - - Start a session by scanning a WalletConnect QR code on a DApp + + ( + <> + + {index < pairings.length - 1 && } + + )} + ListEmptyComponent={ + + Start a session by scanning a WalletConnect QR - - } - contentContainerStyle={styles.contentContainer} - showsVerticalScrollIndicator={false} - estimatedItemSize={ListItemHeight.TRIPLE_LINE} - /> - + } + contentContainerStyle={styles.contentContainer} + showsVerticalScrollIndicator={false} + /> + + router.push(`/scan/`)} /> + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, - }, contentContainer: { - paddingBottom: 8, + flexGrow: 1, }, - emptyContainer: { - marginVertical: 8, - marginHorizontal: 16, - gap: 8, + text: { + margin: 16, }, }); diff --git a/app/src/app/(drawer)/settings/auth.tsx b/app/src/app/(drawer)/settings/auth.tsx index 2f4669300..df5989d2f 100644 --- a/app/src/app/(drawer)/settings/auth.tsx +++ b/app/src/app/(drawer)/settings/auth.tsx @@ -1,11 +1,5 @@ -import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { AuthSettings } from '~/components/shared/AuthSettings'; export default function AuthSettingsScreen() { - return ( - <> - - - - ); + return ; } diff --git a/app/src/app/(drawer)/settings/notifications.tsx b/app/src/app/(drawer)/settings/notifications.tsx index 631911bf1..f56f745b2 100644 --- a/app/src/app/(drawer)/settings/notifications.tsx +++ b/app/src/app/(drawer)/settings/notifications.tsx @@ -1,11 +1,5 @@ -import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import NotificationSettings from '~/components/shared/NotificationSettings'; export default function NotificationSettingsScreen() { - return ( - <> - - - - ); + return ; } diff --git a/app/src/app/(drawer)/transaction/[hash]/_layout.tsx b/app/src/app/(drawer)/transaction/[hash]/_layout.tsx index 9aee6c823..ab644c19a 100644 --- a/app/src/app/(drawer)/transaction/[hash]/_layout.tsx +++ b/app/src/app/(drawer)/transaction/[hash]/_layout.tsx @@ -14,6 +14,7 @@ import { ProposalActions } from '~/components/transaction/ProposalActions'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { useRouter } from 'expo-router'; import { TopTabs } from '~/components/layout/TopTabs'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query TransactionLayout($hash: Bytes32!) { @@ -56,7 +57,7 @@ export default function TransactionLayout() { if (!proposal) return query.stale ? null : ; return ( - + <> ( @@ -77,18 +78,22 @@ export default function TransactionLayout() { )} /> - - - - - + + + + + + + - - + + + + ); } diff --git a/app/src/app/(drawer)/user.tsx b/app/src/app/(drawer)/user.tsx index b6c6c1f7a..c56e858c1 100644 --- a/app/src/app/(drawer)/user.tsx +++ b/app/src/app/(drawer)/user.tsx @@ -18,6 +18,7 @@ import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { AppbarMenu } from '~/components/Appbar/AppbarMenu'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query UserScreen { @@ -55,7 +56,7 @@ function UserScreen() { }); return ( - + <> } /> - { - await update({ name }); - })} - /> - - - Approvers - - {user.approvers.map((approver) => ( - - ))} - - - - - Link - - - - - - showSuccess('Linked Google account')} /> - - - - - - - + + + { + await update({ name }); + })} + /> + + + Approvers + + {user.approvers.map((approver) => ( + + ))} + + + + + Link + + + + + + showSuccess('Linked Google account')} /> + + + + + + + + + ); } diff --git a/app/src/app/_layout.tsx b/app/src/app/_layout.tsx index 05a77e6b5..2e681c230 100644 --- a/app/src/app/_layout.tsx +++ b/app/src/app/_layout.tsx @@ -18,6 +18,7 @@ import { ThemeProvider } from '~/util/theme/ThemeProvider'; import { AppbarHeader } from '~/components/Appbar/AppbarHeader'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; +import { Fonts } from '~/components/Fonts'; const modal: NativeStackNavigationOptions = { presentation: 'modal', @@ -45,7 +46,6 @@ function Layout() { return ( - @@ -56,11 +56,8 @@ function Layout() { - - - - - + + @@ -69,6 +66,7 @@ function Layout() { + ); @@ -77,6 +75,7 @@ function Layout() { export default function RootLayout() { return ( + diff --git a/app/src/app/addresses.tsx b/app/src/app/addresses.tsx index aabc5b90a..18cd96408 100644 --- a/app/src/app/addresses.tsx +++ b/app/src/app/addresses.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; +import { Platform } from 'react-native'; import { NavigateNextIcon, ScanIcon, SearchIcon } from '~/util/theme/icons'; import { AppbarBack } from '~/components/Appbar/AppbarBack'; -import { Searchbar } from '~/components/fields/Searchbar'; +import { Searchbar } from '~/components/Appbar/Searchbar'; import { ListItemHeight } from '~/components/list/ListItem'; import { gql } from '@api/generated'; import { FlashList } from '@shopify/flash-list'; @@ -18,7 +19,6 @@ import { TokenItem } from '~/components/token/TokenItem'; import { z } from 'zod'; import { zAddress, zArray } from '~/lib/zod'; import { useLocalParams } from '~/hooks/useLocalParams'; -import { Stack } from 'expo-router'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; @@ -86,7 +86,6 @@ function AddressesScreen() { return ( - ), ]} - inset={false} + inset={Platform.OS === 'android'} value={query} onChangeText={setQuery} /> diff --git a/app/src/app/approver/[address]/qr.tsx b/app/src/app/approver/[address]/qr.tsx index bbbc73211..8d1a7902d 100644 --- a/app/src/app/approver/[address]/qr.tsx +++ b/app/src/app/approver/[address]/qr.tsx @@ -5,7 +5,7 @@ import { QrModal } from '~/components/QrModal'; export type ApproverQrModalRoute = `/approver/[address]/qr`; export type ApproverQrModalParams = SearchParams; -export function ApproverQrModal() { +export default function ApproverQrModal() { const params = useLocalSearchParams(); return ; diff --git a/app/src/app/index.tsx b/app/src/app/index.tsx index dec990594..bfb496bf8 100644 --- a/app/src/app/index.tsx +++ b/app/src/app/index.tsx @@ -27,6 +27,6 @@ export default function RootScreen() { }} /> ) : ( - + ); } diff --git a/app/src/app/onboard/(drawer)/_layout.tsx b/app/src/app/onboard/(drawer)/_layout.tsx new file mode 100644 index 000000000..58b913e6b --- /dev/null +++ b/app/src/app/onboard/(drawer)/_layout.tsx @@ -0,0 +1,66 @@ +import { Drawer } from '~/components/drawer/Drawer'; +import { DrawerSurface } from '~/components/drawer/DrawerSurface'; +import { Drawer as RnpDrawer } from 'react-native-paper'; +import { DrawerItem } from '~/components/drawer/DrawerItem'; +import { StyleSheet } from 'react-native'; +import { FingerprintIcon, LogoIcon, NotificationsIcon, UserIcon } from '@theme/icons'; +import { DrawerContentComponentProps } from '@react-navigation/drawer'; + +export const unstable_settings = { + initialRouteName: `user`, +}; + +export default function OnboardingDrawerLayout() { + return ; +} + +enum ORDER { + user = 1, + auth, + notifications, +} + +function Content({ state }: DrawerContentComponentProps) { + const position = Math.max( + ORDER.user, + ...state.history + .map( + (route) => + route.type === 'route' && + ORDER[state.routes.find((r) => r.key === route.key)?.name as keyof typeof ORDER], + ) + .filter(Boolean), + ); + + return ( + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + surface: { + paddingTop: 32, + }, + logo: { + minHeight: 88, + marginBottom: 24, + }, +}); diff --git a/app/src/app/onboard/auth.tsx b/app/src/app/onboard/(drawer)/auth.tsx similarity index 92% rename from app/src/app/onboard/auth.tsx rename to app/src/app/onboard/(drawer)/auth.tsx index 4659ce709..418ad1cb0 100644 --- a/app/src/app/onboard/auth.tsx +++ b/app/src/app/onboard/(drawer)/auth.tsx @@ -8,7 +8,7 @@ export default function NotificationsOnboardingScreen() { return ( router.push(`/onboard/notifications`)}> + } diff --git a/app/src/app/onboard/notifications.tsx b/app/src/app/onboard/(drawer)/notifications.tsx similarity index 100% rename from app/src/app/onboard/notifications.tsx rename to app/src/app/onboard/(drawer)/notifications.tsx diff --git a/app/src/app/onboard/(drawer)/user.tsx b/app/src/app/onboard/(drawer)/user.tsx new file mode 100644 index 000000000..adf8bac09 --- /dev/null +++ b/app/src/app/onboard/(drawer)/user.tsx @@ -0,0 +1,121 @@ +import { useRouter } from 'expo-router'; +import { useForm } from 'react-hook-form'; +import { StyleSheet, View } from 'react-native'; +import { FormSubmitButton } from '~/components/fields/FormSubmitButton'; +import { FormTextField } from '~/components/fields/FormTextField'; +import { Actions } from '~/components/layout/Actions'; +import { gql } from '@api/generated'; +import { useMutation } from 'urql'; +import { useQuery } from '~/gql'; +import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; +import { withSuspense } from '~/components/skeleton/withSuspense'; +import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; + +const Query = gql(/* GraphQL */ ` + query UserOnboarding { + user { + id + name + approvers { + id + name + } + } + + approver { + id + name + } + } +`); + +const Update = gql(/* GraphQL */ ` + mutation UserOnboarding_Update($user: String!, $approver: String!) { + updateUser(input: { name: $user }) { + id + name + } + + updateApprover(input: { name: $approver }) { + id + name + } + } +`); + +interface Inputs { + user: string; + approver: string; +} + +function UserOnboardingScreen() { + const router = useRouter(); + const { user, approver } = useQuery(Query).data; + const update = useMutation(Update)[1]; + + const { control, handleSubmit } = useForm({ + defaultValues: { user: user.name ?? '', approver: approver?.name ?? '' }, + }); + + const takenNames = user.approvers.filter((a) => a.id !== approver?.id).map((a) => a.name); + + return ( + <> + + + + + + + + !takenNames.includes(v) || 'An approver with ths name already exists', + }} + /> + + + + { + update({ user, approver }); + router.push(`/onboard/(drawer)/auth`); + })} + > + Continue + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + flex: 1, + alignSelf: 'center', + width: 600, + }, + fields: { + margin: 16, + gap: 16, + }, +}); + +export default withSuspense(UserOnboardingScreen, ); diff --git a/app/src/app/onboard/approver.tsx b/app/src/app/onboard/approver.tsx deleted file mode 100644 index 09d1c8aa4..000000000 --- a/app/src/app/onboard/approver.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useRouter } from 'expo-router'; -import { View } from 'react-native'; -import { Actions } from '~/components/layout/Actions'; -import { StyleSheet } from 'react-native'; -import { useForm } from 'react-hook-form'; -import { FormTextField } from '~/components/fields/FormTextField'; -import { FormSubmitButton } from '~/components/fields/FormSubmitButton'; -import { TextInput } from 'react-native-paper'; -import { gql } from '@api/generated'; -import { useMutation } from 'urql'; -import { useQuery } from '~/gql'; -import { NotFound } from '~/components/NotFound'; -import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; -import { getDeviceModel } from '~/lib/device'; -import { withSuspense } from '~/components/skeleton/withSuspense'; -import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; - -const Query = gql(/* GraphQL */ ` - query ApproverOnboarding { - approver { - id - address - name - } - - user { - id - name - approvers { - id - name - } - } - } -`); - -const Update = gql(/* GraphQL */ ` - mutation ApproverOnboarding_update($name: String!) { - updateApprover(input: { name: $name }) { - id - name - label - } - } -`); - -interface Inputs { - name: string; -} - -function ApproverOnboardingScreen() { - const router = useRouter(); - const query = useQuery(Query); - const { approver, user } = query.data; - const update = useMutation(Update)[1]; - - const { control, handleSubmit } = useForm({ - defaultValues: { name: approver?.name ?? getDeviceModel() ?? '' }, - }); - - if (!approver) return query.stale ? null : ; - - const takenNames = user.approvers.filter((a) => a.id !== approver.id).map((a) => a.name); - - return ( - - - - - : undefined} - label="Label" - placeholder="iPhone" - autoFocus - containerStyle={styles.inset} - rules={{ - required: true, - validate: (v) => !takenNames.includes(v) || 'An approver with ths name already exists', - }} - onBlur={handleSubmit(async ({ name }) => { - await update({ name }); - })} - /> - - - - router.push(`/onboard/auth`)} - > - Continue - - - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, - fields: { - marginVertical: 16, - gap: 16, - }, - inset: { - marginHorizontal: 16, - }, - button: { - alignSelf: 'stretch', - }, -}); - -export default withSuspense(ApproverOnboardingScreen, ); diff --git a/app/src/app/onboard/index.tsx b/app/src/app/onboard/index.tsx deleted file mode 100644 index 43446d183..000000000 --- a/app/src/app/onboard/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Image } from 'expo-image'; -import { SearchParams, Stack, useRouter } from 'expo-router'; -import { StyleSheet, View } from 'react-native'; -import { Text } from 'react-native-paper'; -import { gql } from 'urql'; -import { Button } from '~/components/Button'; -import { Actions } from '~/components/layout/Actions'; -import { LinkAppleButton } from '~/components/link/LinkAppleButton'; -import { LinkGoogleButton } from '~/components/link/LinkGoogleButton'; -import { LinkingButton } from '~/components/link/LinkingButton'; -import { LinkLedgerButton } from '~/components/link/ledger/LinkLedgerButton'; -import { useUrqlApiClient } from '@api/client'; - -const Query = gql(/* GraphQL */ ` - query OnboardScreen { - user { - id - name - } - } -`); - -export type OnboardScreenRoute = `/onboard/`; -export type OnboardScreenParams = SearchParams; - -export default function OnboardScreen() { - const { push } = useRouter(); - const api = useUrqlApiClient(); - - const next = async () => { - const user = (await api.query(Query, {}, { requestPolicy: 'network-only' })).data?.user; - - if (!user?.name) { - push(`/onboard/user`); - } else { - push(`/onboard/approver`); - } - }; - - return ( - - - - - { - console.error(e); - }} - style={{ width: 325, height: 104 }} - /> - - Permission-based{'\n'}self-custodial smart wallet - - - - - - Continue with - - - - - - - - - - - - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, - header: { - flexGrow: 1, - alignItems: 'center', - justifyContent: 'center', - marginHorizontal: 16, - }, - description: { - marginTop: 32, - textAlign: 'center', - }, - continueText: { - textAlign: 'center', - }, - methodsContainer: { - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - gap: 16, - marginBottom: 16, - }, -}); diff --git a/app/src/app/onboard/landing.tsx b/app/src/app/onboard/landing.tsx new file mode 100644 index 000000000..486c9aeec --- /dev/null +++ b/app/src/app/onboard/landing.tsx @@ -0,0 +1,154 @@ +import { useRouter } from 'expo-router'; +import { Platform, View, ScrollView } from 'react-native'; +import { Text } from 'react-native-paper'; +import { Button } from '~/components/Button'; +import { Actions } from '~/components/layout/Actions'; +import { LinkAppleButton } from '~/components/link/LinkAppleButton'; +import { LinkGoogleButton } from '~/components/link/LinkGoogleButton'; +import { LinkingButton } from '~/components/link/LinkingButton'; +import { LinkLedgerButton } from '~/components/link/ledger/LinkLedgerButton'; +import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; +import { + AppScreenshots, + AppStoreBadge, + GithubIcon, + GooglePlayBadge, + LogoIcon, + TwitterIcon, +} from '@theme/icons'; +import { CONFIG } from '~/util/config'; +import { makeStyles } from '@theme/makeStyles'; + +export default function LandingScreen() { + const styles = useStyles(); + const { push } = useRouter(); + + const next = () => push(`/onboard/(drawer)/user`); + + return ( + + null} + trailing={[ + (props) => push(CONFIG.metadata.twitter)} />, + (props) => push(CONFIG.metadata.github)} />, + ]} + /> + + + + + + + + + Self-custody without compromise + + + + + Use your account across all devices + + + + + + + + + + + + + + + + + Continue with + + + + + + + + + + + + + ); +} + +const useStyles = makeStyles(({ layout }) => ({ + root: { + flexGrow: 1, + }, + content: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: 16, + marginVertical: 32, + gap: 8, + flexWrap: 'wrap', + }, + spacer: { + ...(!(Platform.OS === 'web' && layout === 'expanded') && { display: 'none' }), + flex: 1, + }, + mainContent: { + flexGrow: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + }, + logo: { + minWidth: 325, + minHeight: 104, + }, + companionContainer: { + ...(Platform.OS !== 'web' && { display: 'none' }), + gap: 16, + marginVertical: 16, + }, + appStores: { + flexDirection: 'row', + gap: 16, + }, + text: { + textAlign: 'center', + }, + appStoreBadge: { + width: 143, + height: 48, + }, + playStoreBadge: { + width: 161, + height: 48, + }, + screenshotsContainer: { + ...(!(Platform.OS === 'web' && layout === 'expanded') && { display: 'none' }), + flex: 1, + alignItems: 'center', + minWidth: 'auto', + }, + screenshots: { + width: 413, + height: 453, + }, + actionsContainer: { + alignSelf: 'center', + maxWidth: 1000, + width: '100%', + }, + methodsContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: 16, + marginBottom: 16, + }, +})); diff --git a/app/src/app/onboard/user.tsx b/app/src/app/onboard/user.tsx deleted file mode 100644 index c7e16c6e6..000000000 --- a/app/src/app/onboard/user.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useRouter } from 'expo-router'; -import { useForm } from 'react-hook-form'; -import { StyleSheet, View } from 'react-native'; -import { FormSubmitButton } from '~/components/fields/FormSubmitButton'; -import { FormTextField } from '~/components/fields/FormTextField'; -import { Actions } from '~/components/layout/Actions'; -import { gql } from '@api/generated'; -import { useMutation } from 'urql'; -import { useQuery } from '~/gql'; -import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; -import { withSuspense } from '~/components/skeleton/withSuspense'; -import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; - -const Query = gql(/* GraphQL */ ` - query CreateUser { - user { - id - name - } - } -`); - -const Update = gql(/* GraphQL */ ` - mutation CreateUserScreen_Update($name: String!) { - updateUser(input: { name: $name }) { - id - name - } - } -`); - -interface Inputs { - name: string; -} - -function UserOnboardingScreen() { - const router = useRouter(); - const { user } = useQuery(Query).data; - const update = useMutation(Update)[1]; - - const { control, handleSubmit } = useForm({ - defaultValues: { name: user.name ?? '' }, - }); - - return ( - - - - { - await update({ name }); - })} - /> - - - router.push(`/onboard/approver`)} - > - Continue - - - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, - nameContainer: { - margin: 16, - }, - actionButton: { - alignSelf: 'stretch', - }, -}); - -export default withSuspense(UserOnboardingScreen, ); diff --git a/app/src/app/[account]/policies/[key]/[contract]/add-selector.tsx b/app/src/app/selector.tsx similarity index 64% rename from app/src/app/[account]/policies/[key]/[contract]/add-selector.tsx rename to app/src/app/selector.tsx index 2db558500..fcc71b4ac 100644 --- a/app/src/app/[account]/policies/[key]/[contract]/add-selector.tsx +++ b/app/src/app/selector.tsx @@ -1,26 +1,26 @@ -import { useImmerAtom } from 'jotai-immer'; -import { ContractPermissionsScheme } from '~/app/(drawer)/[account]/policies/[key]/[contract]'; -import { useLocalParams } from '~/hooks/useLocalParams'; -import { POLICY_DRAFT_ATOM } from '~/lib/policy/draft'; -import { Selector } from 'lib'; +import { Selector, asSelector } from 'lib'; import { useForm } from 'react-hook-form'; import { FormTextField } from '~/components/fields/FormTextField'; import { StyleSheet, View } from 'react-native'; import { Actions } from '~/components/layout/Actions'; import { FormSubmitButton } from '~/components/fields/FormSubmitButton'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; +import { Subject } from 'rxjs'; +import { useGetEvent } from '~/hooks/useGetEvent'; + +const SELECTOR_ADDED = new Subject(); + +export function useGetSelector() { + const getEvent = useGetEvent(); + + return () => getEvent(`/selector`, SELECTOR_ADDED); +} interface Inputs { selector: string; } -export default function AddSelectorModal() { - const { account } = useLocalParams( - `/[account]/policies/[key]/[contract]/add-selector`, - ContractPermissionsScheme, - ); - - const [, updatePolicy] = useImmerAtom(POLICY_DRAFT_ATOM); +export default function SelectorModal() { const { control, handleSubmit } = useForm(); return ( @@ -47,12 +47,10 @@ export default function AddSelectorModal() { mode="contained" control={control} onPress={handleSubmit(({ selector }) => { - updatePolicy((draft) => { - draft.permissions.targets.contracts[account].functions[selector as Selector] = true; - }); + SELECTOR_ADDED.next(asSelector(selector)); })} > - Add interaction + Add action diff --git a/app/src/components/Appbar/AppbarMenu.tsx b/app/src/components/Appbar/AppbarMenu.tsx index 0765f23df..16e7569ba 100644 --- a/app/src/components/Appbar/AppbarMenu.tsx +++ b/app/src/components/Appbar/AppbarMenu.tsx @@ -1,17 +1,21 @@ import { materialCommunityIcon } from '@theme/icons'; -import { ComponentPropsWithoutRef } from 'react'; -import { useDrawerActions, useDrawerContext } from '~/components/drawer/DrawerContextProvider'; +import { ComponentPropsWithoutRef, FC } from 'react'; +import { P, match } from 'ts-pattern'; +import { useDrawerActions, useMaybeDrawerContext } from '~/components/drawer/DrawerContextProvider'; const MenuIcon = materialCommunityIcon('menu'); -export interface AppbarMenuProps - extends Omit, 'onPress'> {} +type BaseProps = Omit, 'onPress'>; -export function AppbarMenu(props: AppbarMenuProps) { - const { type } = useDrawerContext(); - const { toggle } = useDrawerActions(); +export interface AppbarMenuProps extends BaseProps { + fallback?: FC; +} - if (type === 'standard') return null; +export function AppbarMenu({ fallback: Fallback, ...props }: AppbarMenuProps) { + const type = useMaybeDrawerContext()?.type; + const { toggle } = useDrawerActions(); - return ; + return match(type) + .with(P.union('standard', P.nullish), () => (Fallback ? : null)) + .otherwise(() => ); } diff --git a/app/src/components/fields/Searchbar.tsx b/app/src/components/Appbar/Searchbar.tsx similarity index 81% rename from app/src/components/fields/Searchbar.tsx rename to app/src/components/Appbar/Searchbar.tsx index 85e24507b..e136b23b5 100644 --- a/app/src/components/fields/Searchbar.tsx +++ b/app/src/components/Appbar/Searchbar.tsx @@ -1,35 +1,31 @@ -import { IconProps } from '@theme/icons'; +import { IconProps, SearchIcon } from '@theme/icons'; import { makeStyles } from '@theme/makeStyles'; import { toArray } from 'lib'; import { FC } from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; import { Surface } from 'react-native-paper'; -import { BasicTextField, BasicTextFieldProps } from './BasicTextField'; +import { BasicTextField, BasicTextFieldProps } from '../fields/BasicTextField'; export interface SearchbarProps extends BasicTextFieldProps { leading?: FC }>; trailing?: FC | FC[]; - placeholder: string; + placeholder?: string; inset?: boolean; } -export const Searchbar = ({ - leading: Leading, +export function Searchbar({ + leading: Leading = SearchIcon, trailing, inset = true, ...inputProps -}: SearchbarProps) => { +}: SearchbarProps) { const styles = useStyles(inset); return ( - {Leading && ( - - )} + + {Leading && } + @@ -40,7 +36,7 @@ export const Searchbar = ({ ); -}; +} const useStyles = makeStyles(({ colors, corner, fonts, insets }, inset: boolean) => ({ // https://m3.material.io/components/search/specs diff --git a/app/src/components/Appbar/SearchbarHeader.tsx b/app/src/components/Appbar/SearchbarHeader.tsx new file mode 100644 index 000000000..47653933b --- /dev/null +++ b/app/src/components/Appbar/SearchbarHeader.tsx @@ -0,0 +1,14 @@ +import { NativeStackHeaderProps } from '@react-navigation/native-stack'; +import { DrawerHeaderProps } from '@react-navigation/drawer'; +import type { SearchbarOptionsProps } from './SearchbarOptions'; +import { Searchbar } from '~/components/Appbar/Searchbar'; + +type NavHeaderProps = NativeStackHeaderProps | DrawerHeaderProps; + +export interface SearchbarHeaderProps extends Omit { + options: NavHeaderProps['options'] & { searchbar?: SearchbarOptionsProps }; +} + +export function SearchbarHeader({ options }: SearchbarHeaderProps) { + return ; +} diff --git a/app/src/components/Appbar/SearchbarOptions.tsx b/app/src/components/Appbar/SearchbarOptions.tsx new file mode 100644 index 000000000..023a47c43 --- /dev/null +++ b/app/src/components/Appbar/SearchbarOptions.tsx @@ -0,0 +1,16 @@ +import { useNavigation } from 'expo-router'; +import { useLayoutEffect } from 'react'; +import { SearchbarProps } from './Searchbar'; +import { SearchbarHeader } from './SearchbarHeader'; + +export interface SearchbarOptionsProps extends SearchbarProps {} + +export function SearchbarOptions(options: SearchbarOptionsProps) { + const navigation = useNavigation(); + + useLayoutEffect(() => { + navigation.setOptions({ header: SearchbarHeader, searchbar: options }); + }, [navigation.setOptions, options]); + + return null; +} diff --git a/app/src/components/Fonts.tsx b/app/src/components/Fonts.tsx new file mode 100644 index 000000000..ace4d7e90 --- /dev/null +++ b/app/src/components/Fonts.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { SplashScreen } from 'expo-router'; +import { loadAsync } from 'expo-font'; +import { Font } from 'react-native-paper/lib/typescript/types'; +import { logError } from '~/util/analytics'; + +const FONTS = { + Roboto: require('assets/fonts/Roboto-400-Regular.ttf'), + 'Roboto-Medium': require('assets/fonts/Roboto-500-Medium.ttf'), +} as const; + +export const FONT_BY_WEIGHT: Partial< + Record, keyof typeof FONTS & Font['fontFamily']> +> = { + '400': 'Roboto', + '500': 'Roboto-Medium', +}; + +SplashScreen.preventAutoHideAsync(); + +export function Fonts() { + useEffect(() => { + loadAsync(FONTS) + .catch((error) => logError('Failed to load fonts', { error })) + .finally(SplashScreen.hideAsync); + }, []); + + return null; +} diff --git a/app/src/components/HideSplash.tsx b/app/src/components/HideSplash.tsx deleted file mode 100644 index 97447d170..000000000 --- a/app/src/components/HideSplash.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useEffect } from 'react'; -import { SplashScreen } from 'expo-router'; - -export const HideSplash = () => { - useEffect(() => { - SplashScreen.hideAsync(); - }, []); - - return null; -}; diff --git a/app/src/components/InputsView.tsx b/app/src/components/InputsView.tsx index f478bec32..643ca84ba 100644 --- a/app/src/components/InputsView.tsx +++ b/app/src/components/InputsView.tsx @@ -109,7 +109,6 @@ export const InputsView = ({ input, setInput, type, setType, ...props }: InputsV const useStyles = makeStyles(({ colors }) => ({ container: { - alignItems: 'center', marginHorizontal: 16, marginVertical: 32, }, @@ -134,7 +133,8 @@ const useStyles = makeStyles(({ colors }) => ({ textAlign: 'center', }, balanceWarning: { - marginVertical: 8, color: colors.warning, + textAlign: 'center', + marginVertical: 8, }, })); diff --git a/app/src/components/drawer/Drawer.tsx b/app/src/components/drawer/Drawer.tsx index c30a9958b..a6aa5b57c 100644 --- a/app/src/components/drawer/Drawer.tsx +++ b/app/src/components/drawer/Drawer.tsx @@ -1,6 +1,5 @@ import { ComponentPropsWithoutRef } from 'react'; import { Drawer as DrawerLayout } from 'expo-router/drawer'; -import { DrawerContent } from '~/components/drawer/DrawerContent'; import { makeStyles } from '@theme/makeStyles'; import { useLayout } from '~/hooks/useLayout'; import { DrawerContextProvider, DrawerType } from './DrawerContextProvider'; @@ -10,7 +9,6 @@ type DrawerLayoutProps = ComponentPropsWithoutRef; export interface DrawerProps extends DrawerLayoutProps {} -// TODO: only use 'modal' type in compact and medium layouts export function Drawer({ children, ...props }: DrawerProps) { const { layout } = useLayout(); const type: DrawerType = layout === 'expanded' ? 'standard' : 'modal'; @@ -19,7 +17,6 @@ export function Drawer({ children, ...props }: DrawerProps) { return ( @@ -39,12 +37,22 @@ export function Drawer({ children, ...props }: DrawerProps) { Drawer.Screen = DrawerLayout.Screen; -const useStyles = makeStyles(({ colors }, type: DrawerType) => ({ - drawer: { - backgroundColor: 'transparent', - width: 360, - }, - overlay: { - backgroundColor: colors.scrim, - }, -})); +const useStyles = makeStyles(({ colors }, type: DrawerType) => { + const backgroundColor = type === 'standard' ? colors.elevation.level1 : 'transparent'; + + return { + drawer: { + backgroundColor, + width: 360, + // Unset borders + borderLeftWidth: undefined, + borderRightWidth: undefined, + }, + overlay: { + backgroundColor: colors.scrim, + }, + sceneContainer: { + backgroundColor, + }, + }; +}); diff --git a/app/src/components/drawer/DrawerContent.tsx b/app/src/components/drawer/DrawerContent.tsx deleted file mode 100644 index 5e30f18ce..000000000 --- a/app/src/components/drawer/DrawerContent.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Surface, SurfaceProps } from 'react-native-paper'; -import { ScrollView } from 'react-native'; -import { makeStyles } from '@theme/makeStyles'; -import { Drawer as PaperDrawer } from 'react-native-paper'; -import { - ContactsIcon, - FingerprintIcon, - NotificationsIcon, - TransferIcon, - SwapIcon, - HomeIcon, - WalletConnectIcon, - TwitterIcon, - GithubIcon, - PolicyIcon, - QrCodeIcon, -} from '@theme/icons'; -import { DrawerType, useDrawerContext } from './DrawerContextProvider'; -import { DrawerItem as Item } from './DrawerItem'; -import { useSelectedAccount } from '~/hooks/useSelectedAccount'; -import { Image } from 'expo-image'; -import { ETH_ICON_URI } from '~/components/token/TokenIcon'; -import { CONFIG } from '~/util/config'; -import { UserHeader } from '~/components/drawer/UserHeader'; -import { useTransfer } from '~/hooks/useTransfer'; - -const Section = PaperDrawer.Section; - -const surfaceTypeProps: Record> = { - standard: { mode: 'flat', elevation: 0 }, - modal: { mode: 'elevated', elevation: 1 }, -}; - -export interface DrawerContentProps {} - -export function DrawerContent() { - const styles = useStyles(); - const { type } = useDrawerContext(); - const account = useSelectedAccount(); - const transfer = useTransfer(); - - return ( - - -
- - - {account ? ( - - ) : ( - - )} - - - - - {account && ( - } - label="Tokens" - /> - )} -
- - {account && ( -
- transfer({ account })} - /> - - - - -
- )} - -
- {account && ( - - )} - - -
- -
- - -
-
-
- ); -} - -const useStyles = makeStyles(({ corner, insets, iconSize }) => ({ - container: { - flex: 1, - borderTopRightRadius: corner.l, - borderBottomRightRadius: corner.l, - }, - contentContainer: { - paddingTop: insets.top + 12, - paddingBottom: insets.bottom, - }, - icon: { - width: iconSize.small, - height: iconSize.small, - }, -})); diff --git a/app/src/components/drawer/DrawerContextProvider.tsx b/app/src/components/drawer/DrawerContextProvider.tsx index c9d5d90d1..0015b2943 100644 --- a/app/src/components/drawer/DrawerContextProvider.tsx +++ b/app/src/components/drawer/DrawerContextProvider.tsx @@ -4,6 +4,10 @@ import { ReactNode, createContext, useContext } from 'react'; const CONTEXT = createContext(undefined); +export function useMaybeDrawerContext() { + return useContext(CONTEXT); +} + export function useDrawerContext() { const context = useContext(CONTEXT); if (!context) throw new Error('Drawer context not found'); diff --git a/app/src/components/drawer/DrawerItem.tsx b/app/src/components/drawer/DrawerItem.tsx index 0a4c7a5a6..e1c2bf049 100644 --- a/app/src/components/drawer/DrawerItem.tsx +++ b/app/src/components/drawer/DrawerItem.tsx @@ -1,32 +1,39 @@ +import { IconProps } from '@theme/icons'; +import { useTheme } from '@theme/paper'; import { Href, Link, useRouter, useSegments } from 'expo-router'; -import { ComponentPropsWithoutRef } from 'react'; +import { ComponentPropsWithoutRef, FC } from 'react'; import { Drawer } from 'react-native-paper'; -import { IconSource } from 'react-native-paper/lib/typescript/components/Icon'; import { useDrawerActions } from '~/components/drawer/DrawerContextProvider'; export interface DrawerItemProps extends Pick, 'onPress'> { href: Href; label: string; - icon?: IconSource; + icon?: FC; + disabled?: boolean; } -export function DrawerItem({ href, label, icon, ...props }: DrawerItemProps) { +export function DrawerItem({ href, label, icon: Icon, disabled, ...props }: DrawerItemProps) { const currentPath = `/${useSegments().join('/')}`; const hrefPath = getHrefPath(href); const router = useRouter(); const { close } = useDrawerActions(); + const { stateLayer } = useTheme(); return ( // { router.push(href); close(); }} + {...(disabled && { + onPress: undefined, + icon: Icon && ((props) => ), + })} {...props} /> // diff --git a/app/src/components/drawer/DrawerSurface.tsx b/app/src/components/drawer/DrawerSurface.tsx new file mode 100644 index 000000000..daac43d05 --- /dev/null +++ b/app/src/components/drawer/DrawerSurface.tsx @@ -0,0 +1,38 @@ +import { Surface, SurfaceProps } from 'react-native-paper'; +import { ScrollView, ScrollViewProps } from 'react-native'; +import { makeStyles } from '@theme/makeStyles'; +import { DrawerType, useDrawerContext } from './DrawerContextProvider'; + +const surfaceTypeProps: Record> = { + standard: { mode: 'flat', elevation: 0 }, + modal: { mode: 'elevated', elevation: 1 }, +}; + +export interface DrawerSurfaceProps extends ScrollViewProps {} + +export function DrawerSurface(props: DrawerSurfaceProps) { + const styles = useStyles(); + const { type } = useDrawerContext(); + + return ( + + + + ); +} + +const useStyles = makeStyles(({ corner, insets }) => ({ + surface: { + flex: 1, + borderTopRightRadius: corner.l, + borderBottomRightRadius: corner.l, + }, + container: { + paddingTop: insets.top + 16, + paddingBottom: insets.bottom, + }, +})); diff --git a/app/src/components/drawer/RootDrawer.tsx b/app/src/components/drawer/RootDrawer.tsx new file mode 100644 index 000000000..0d4beea90 --- /dev/null +++ b/app/src/components/drawer/RootDrawer.tsx @@ -0,0 +1,114 @@ +import { StyleSheet } from 'react-native'; +import { Drawer as PaperDrawer } from 'react-native-paper'; +import { + ContactsIcon, + FingerprintIcon, + NotificationsIcon, + TransferIcon, + SwapIcon, + HomeIcon, + WalletConnectIcon, + TwitterIcon, + GithubIcon, + PolicyIcon, + QrCodeIcon, +} from '@theme/icons'; +import { DrawerItem as Item } from './DrawerItem'; +import { useSelectedAccount } from '~/hooks/useSelectedAccount'; +import { Image } from 'expo-image'; +import { ETH_ICON_URI } from '~/components/token/TokenIcon'; +import { CONFIG } from '~/util/config'; +import { UserHeader } from '~/components/drawer/UserHeader'; +import { useTransfer } from '~/hooks/useTransfer'; +import { DrawerContentComponentProps } from '@react-navigation/drawer'; +import { DrawerSurface } from './DrawerSurface'; +import { ICON_SIZE } from '@theme/paper'; + +const Section = PaperDrawer.Section; + +export interface RootDrawerProps extends DrawerContentComponentProps {} + +export function RootDrawer(_props: RootDrawerProps) { + const account = useSelectedAccount(); + const transfer = useTransfer(); + + return ( + +
+ + + {account ? ( + + ) : ( + + )} + + + + + {account && ( + } + label="Tokens" + /> + )} +
+ + {account && ( +
+ transfer({ account })} + /> + + + + +
+ )} + +
+ {account && ( + + )} + + +
+ +
+ + +
+
+ ); +} + +const styles = StyleSheet.create({ + icon: { + width: ICON_SIZE.small, + height: ICON_SIZE.small, + }, +}); diff --git a/app/src/components/drawer/UserHeader.tsx b/app/src/components/drawer/UserHeader.tsx index 4fb6bd328..16ea317df 100644 --- a/app/src/components/drawer/UserHeader.tsx +++ b/app/src/components/drawer/UserHeader.tsx @@ -1,10 +1,10 @@ import { gql } from '@api'; import { makeStyles } from '@theme/makeStyles'; import { ICON_SIZE } from '@theme/paper'; +import { Image } from 'expo-image'; import { useRouter } from 'expo-router'; import { TouchableOpacity } from 'react-native'; -import { Text } from 'react-native-paper'; -import { AddressIcon } from '~/components/Identicon/AddressIcon'; +import { Text, Avatar } from 'react-native-paper'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { useQuery } from '~/gql'; @@ -13,12 +13,7 @@ const Query = gql(/* GraphQL */ ` user { id name - } - - approver { - id - address - name + photoUri } } `); @@ -27,21 +22,23 @@ function UserHeader_() { const styles = useStyles(); const router = useRouter(); - const { user, approver } = useQuery(Query).data; - - if (!approver) return null; + const { user } = useQuery(Query).data; return ( - router.push(`/(drawer)/user`)}> - + router.push(`/(drawer)/user`)}> + {user.photoUri ? ( + + ) : ( + + )} {user.name} - - - {approver.name} - ); } @@ -49,15 +46,32 @@ function UserHeader_() { export const UserHeader = withSuspense(UserHeader_); const useStyles = makeStyles(({ colors }) => ({ - userContainer: { + container: { alignItems: 'center', marginHorizontal: 16, marginBottom: 16, }, + icon: { + width: ICON_SIZE.large, + height: ICON_SIZE.large, + borderRadius: ICON_SIZE.large / 2, + }, + avatar: { + backgroundColor: colors.primary, + }, + avatarLabel: { + color: colors.onPrimary, + }, userName: { marginTop: 8, - }, - approverItem: { - color: colors.tertiary, + color: colors.onSurfaceVariant, }, })); + +function getAvatarLabel(name: string | null | undefined) { + // Fnu Lnu -> FL + return (name ?? '?') + .split(' ') + .map((v) => v[0]) + .join(''); +} diff --git a/app/src/components/layout/ScreenSurface.tsx b/app/src/components/layout/ScreenSurface.tsx new file mode 100644 index 000000000..cf0a9f283 --- /dev/null +++ b/app/src/components/layout/ScreenSurface.tsx @@ -0,0 +1,25 @@ +import { makeStyles } from '@theme/makeStyles'; +import { Surface, SurfaceProps } from 'react-native-paper'; +import { useMaybeDrawerContext } from '~/components/drawer/DrawerContextProvider'; + +export interface ScreenSurfaceProps extends SurfaceProps {} + +export function ScreenSurface(props: ScreenSurfaceProps) { + const styles = useStyles(); + const drawer = useMaybeDrawerContext(); + + if (drawer?.type !== 'standard') return <>{props.children}; + + return ; +} + +const useStyles = makeStyles(({ colors, corner }) => ({ + container: { + flex: 1, + marginTop: 8, + marginBottom: 16, + marginRight: 16, + borderRadius: corner.l, + backgroundColor: colors.background, + }, +})); diff --git a/app/src/components/shared/AuthSettings.tsx b/app/src/components/shared/AuthSettings.tsx index 2e2ba8cc2..9cc2a3260 100644 --- a/app/src/components/shared/AuthSettings.tsx +++ b/app/src/components/shared/AuthSettings.tsx @@ -1,25 +1,27 @@ -import { ICON_SIZE } from '@theme/paper'; -import { StyleSheet, View } from 'react-native'; -import { Text } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; import { useImmerAtom } from 'jotai-immer'; import { Switch } from 'react-native-paper'; import { Actions } from '~/components/layout/Actions'; import { ListItem } from '~/components/list/ListItem'; -import { FingerprintIcon } from '@theme/icons'; +import { LockOpenIcon, TransferIcon } from '@theme/icons'; import { ListHeader } from '~/components/list/ListHeader'; import { atom, useAtomValue } from 'jotai'; import { AUTH_SETTINGS_ATOM, SUPPORTS_BIOMETRICS } from '~/components/provider/AuthGate'; import { ReactNode, useEffect } from 'react'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; +import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; +import { AppbarMenu } from '~/components/Appbar/AppbarMenu'; const biometricsAvailableAtom = atom(SUPPORTS_BIOMETRICS); export interface AuthSettingsProps { actions?: ReactNode; + appbarMenu?: boolean; } -function AuthSettings_({ actions }: AuthSettingsProps) { +function AuthSettings_({ actions, appbarMenu }: AuthSettingsProps) { const hasSupport = useAtomValue(biometricsAvailableAtom); const [settings, updateSettings] = useImmerAtom(AUTH_SETTINGS_ATOM); @@ -30,24 +32,22 @@ function AuthSettings_({ actions }: AuthSettingsProps) { }, [hasSupport, settings.open, updateSettings]); return ( - - - + <> + - - Authentication - - - - - Require biometrics when + + Required } + leading={LockOpenIcon} headline="Opening the app" trailing={({ disabled }) => ( updateSettings((s) => ({ ...s, open: !s.open }))} disabled={disabled} /> @@ -56,27 +56,27 @@ function AuthSettings_({ actions }: AuthSettingsProps) { /> } + leading={TransferIcon} headline="Approving a proposal" trailing={({ disabled }) => ( updateSettings((s) => ({ ...s, approval: !s.approval }))} disabled={disabled} /> )} disabled={!hasSupport} /> - - {actions} - + {actions} + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, + surface: { + paddingTop: 8, }, header: { alignItems: 'center', diff --git a/app/src/components/shared/ContactScreen.tsx b/app/src/components/shared/ContactScreen.tsx index 0eb3213f8..d7ec93bd8 100644 --- a/app/src/components/shared/ContactScreen.tsx +++ b/app/src/components/shared/ContactScreen.tsx @@ -22,6 +22,7 @@ import { useConfirmRemoval } from '~/hooks/useConfirm'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Fragment = gql(/* GraphQL */ ` fragment ContactScreen_contact on Contact { @@ -83,7 +84,7 @@ function ContactScreen_({ address }: ContactScreenProps) { }); return ( - + <> - - - - + + + + + + + + + + + + + + [chain.name, chain.name] as const, + ), + ]} + chipProps={{ icon: NetworkIcon, disabled: true }} + onChange={(name) => { + // TODO: global address + }} + /> + - - - + - - - - [chain.name, chain.name] as const), - ]} - chipProps={{ icon: NetworkIcon, disabled: true }} - onChange={(name) => { - // TODO: global address - }} - /> - - - - - - {current ? 'Update' : 'Add'} - - - + style={styles.action} + > + {current ? 'Update' : 'Add'} + + + + ); } const styles = StyleSheet.create({ - container: { - flex: 1, - }, fields: { gap: 16, - marginHorizontal: 16, + margin: 16, }, fieldContainer: { flexDirection: 'row', diff --git a/app/src/components/shared/NotificationSettings.tsx b/app/src/components/shared/NotificationSettings.tsx index 816634664..4db151fb5 100644 --- a/app/src/components/shared/NotificationSettings.tsx +++ b/app/src/components/shared/NotificationSettings.tsx @@ -1,6 +1,4 @@ -import { ICON_SIZE } from '@theme/paper'; -import { StyleSheet, View } from 'react-native'; -import { Text } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; import { persistedAtom } from '~/lib/persistedAtom'; import { useImmerAtom } from 'jotai-immer'; import * as Notifications from 'expo-notifications'; @@ -9,22 +7,31 @@ import { Switch } from 'react-native-paper'; import { Actions } from '~/components/layout/Actions'; import { Button } from '~/components/Button'; import { ListItem } from '~/components/list/ListItem'; -import { NotificationsOutlineIcon } from '@theme/icons'; import { useAtomValue } from 'jotai'; import { ListHeader } from '~/components/list/ListHeader'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; +import { ActivityIcon, IconProps, TransferIcon, UpdateIcon } from '@theme/icons'; +import { FC } from 'react'; +import { AppbarMenu } from '~/components/Appbar/AppbarMenu'; export type NotificationChannel = 'product' | 'activity' | 'transfers'; -export const NotificationChannelConfig: Record = { +export const NotificationChannelConfig: Record< + NotificationChannel, + NotificationChannelInput & { icon?: FC } +> = { product: { name: 'Product updates', description: 'New features and announcements', + icon: UpdateIcon, importance: Notifications.AndroidImportance.DEFAULT, }, transfers: { name: 'Transfers', - description: 'Receipt of tokens and spending allowances', + description: 'Incoming tokens and allowances', + icon: TransferIcon, importance: Notifications.AndroidImportance.DEFAULT, showBadge: false, enableLights: false, @@ -32,7 +39,8 @@ export const NotificationChannelConfig: Record ({ export interface NotificationSettingsProps { next?: () => void; + appbarMenu?: boolean; } -function NotificationSettings({ next }: NotificationSettingsProps) { +function NotificationSettings({ next, appbarMenu }: NotificationSettingsProps) { const [settings, update] = useImmerAtom(NOTIFICATIONS_ATOM); const [perm, requestPerm] = Notifications.usePermissions({ @@ -74,21 +83,20 @@ function NotificationSettings({ next }: NotificationSettingsProps) { }); return ( - - - + <> + - - Notifications - - - - - Receive notifications for + + Receive {Object.entries(NotificationChannelConfig).map(([channel, config]) => ( ))} - - - {!perm?.granted && next && } + + {!perm?.granted && next && } - {(!perm?.granted || next) && ( - - )} - - + {(!perm?.granted || next) && ( + + )} + + + ); } const styles = StyleSheet.create({ - root: { - flex: 1, - }, - header: { - alignItems: 'center', - gap: 4, - marginVertical: 32, - marginHorizontal: 16, - }, - text: { - textAlign: 'center', - marginBottom: 8, + surface: { + paddingTop: 8, }, }); diff --git a/app/src/components/shared/TokenScreen.tsx b/app/src/components/shared/TokenScreen.tsx index b93384c76..320486f9e 100644 --- a/app/src/components/shared/TokenScreen.tsx +++ b/app/src/components/shared/TokenScreen.tsx @@ -21,6 +21,7 @@ import { useConfirmRemoval } from '~/hooks/useConfirm'; import { AppbarOptions } from '~/components/Appbar/AppbarOptions'; import { withSuspense } from '~/components/skeleton/withSuspense'; import { ScreenSkeleton } from '~/components/skeleton/ScreenSkeleton'; +import { ScreenSurface } from '~/components/layout/ScreenSurface'; const Query = gql(/* GraphQL */ ` query TokenScreen($token: Address!) { @@ -118,7 +119,7 @@ function TokenScreen(props: TokenScreenProps) { const [isValidIcon, setValidIcon] = useState(false); return ( - + <> - - : undefined - } - > - - + + + + : undefined + } + > + + - - : undefined - } - > - - + + : undefined + } + > + + - - - + + + - - - + + + - - - + + + - ( - setValidIcon(false)} - onLoad={() => setValidIcon(true)} - style={{ width: size, height: size }} - /> - ) - : UnknownTokenIcon - } - > - !v || isValidIcon || 'Invalid', - }} - control={control} - containerStyle={styles.field} - /> - + ( + setValidIcon(false)} + onLoad={() => setValidIcon(true)} + style={{ width: size, height: size }} + /> + ) + : UnknownTokenIcon + } + > + !v || isValidIcon || 'Invalid', + }} + control={control} + containerStyle={styles.field} + /> + - - { - await upsert({ - input: { ...input, decimals: parseFloat(input.decimals as unknown as string) }, - }); - })} - > - Save - - - + + { + await upsert({ + input: { ...input, decimals: parseFloat(input.decimals as unknown as string) }, + }); + })} + > + Save + + + + + ); } const styles = StyleSheet.create({ container: { flexGrow: 1, - marginTop: 8, + marginTop: 16, marginHorizontal: 16, - gap: 8, + gap: 16, }, field: { flex: 1, diff --git a/app/src/components/tab/TopTabBar.tsx b/app/src/components/tab/TopTabBar.tsx index 1a0679a93..5020a98f2 100644 --- a/app/src/components/tab/TopTabBar.tsx +++ b/app/src/components/tab/TopTabBar.tsx @@ -147,7 +147,7 @@ interface StyleParams { const useStyles = makeStyles(({ colors, fonts }, { variant, withIcon }: StyleParams) => ({ container: { - backgroundColor: colors.surface, + backgroundColor: 'transparent', height: withIcon ? 64 : 48, // Divider borderBottomWidth: StyleSheet.hairlineWidth, diff --git a/app/src/hooks/useCreateFirstAccount.ts b/app/src/hooks/useCreateFirstAccount.ts index c57133477..c2def62a5 100644 --- a/app/src/hooks/useCreateFirstAccount.ts +++ b/app/src/hooks/useCreateFirstAccount.ts @@ -1,9 +1,13 @@ import { gql } from '@api'; +import { useApproverAddress } from '@network/useApprover'; import { useRouter } from 'expo-router'; +import { Address } from 'lib'; +import { useMutation } from 'urql'; +import { showError } from '~/components/provider/SnackbarProvider'; import { useQuery } from '~/gql'; const Query = gql(/* GraphQL */ ` - query CreateFirstAccount { + query UseCreateFirstAccount { accounts { id address @@ -11,15 +15,41 @@ const Query = gql(/* GraphQL */ ` } `); +const Create = gql(/* GraphQL */ ` + mutation UseCreateFirstAccount_Create($input: CreateAccountInput!) { + createAccount(input: $input) { + id + address + } + } +`); + export function useCreateFirsAccount() { const router = useRouter(); + const approver = useApproverAddress(); + const create = useMutation(Create)[1]; + const { accounts } = useQuery(Query).data; - return async () => - accounts?.length - ? router.push({ - pathname: `/(drawer)/[account]/(home)/`, - params: { account: accounts[0].address }, + const nav = (account: Address) => + router.push({ pathname: `/(drawer)/[account]/(home)/`, params: { account } }); + + return async () => { + if (accounts?.length) return nav(accounts[0].address); + + try { + const account = ( + await create({ + input: { + name: 'Personal', + policies: [{ name: 'High risk', approvers: [approver] }], + }, }) - : router.push(`/accounts/create`); + ).data?.createAccount; + + nav(account!.address); + } catch (error) { + showError('Failed to create account', { event: { error } }); + } + }; } diff --git a/app/src/util/theme/icons.tsx b/app/src/util/theme/icons.tsx index b6188b307..b29ac20c9 100644 --- a/app/src/util/theme/icons.tsx +++ b/app/src/util/theme/icons.tsx @@ -1,6 +1,6 @@ import { Image, ImageProps, ImageSource } from 'expo-image'; import { ComponentPropsWithoutRef, ElementType, FC } from 'react'; -import { ColorValue, FlexStyle, ImageStyle, StyleProp, TextStyle } from 'react-native'; +import { ColorValue, TouchableOpacity, TouchableOpacityProps } from 'react-native'; import { Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons'; @@ -108,6 +108,8 @@ export const NotificationsIcon = materialIcon('notifications'); export const NotificationsOutlineIcon = materialIcon('notifications-none'); export const BluetoothIcon = materialIcon('bluetooth'); export const FingerprintIcon = materialIcon('fingerprint'); +export const LockOpenIcon = materialIcon('lock-open'); +export const UpdateIcon = materialIcon('update'); export const PolicyIcon = materialCommunityIcon('security'); export const PolicyActiveIcon = materialCommunityIcon('shield'); @@ -123,7 +125,11 @@ export const PolicySatisfiableOutlineIcon = materialCommunityIcon('shield-check- export const PolicyUnsatisfiableIcon = materialCommunityIcon('shield-alert'); export const PolicyUnsatisfiableOutlineIcon = materialCommunityIcon('shield-alert-outline'); +export const ZalloIcon = fromSource(require('assets/icon-rounded.svg')); export const LogoIcon = fromSource(require('assets/logo.svg')); +export const AppScreenshots = fromSource(require('assets/screenshots.png')); +export const AppStoreBadge = fromSource(require('assets/app-store-badge.svg')); +export const GooglePlayBadge = fromSource(require('assets/google-play-badge.png')); export const MastercardIcon = fromSource(require('assets/mastercard.svg')); export const WalletConnectIcon = fromSource(require('assets/walletconnect.svg')); export const TwitterIcon = fromSource(require('assets/twitter.svg')); @@ -133,17 +139,16 @@ export const LedgerLogo = fromSource(require('assets/ledger-logo.svg')); export const AppleIcon = fromSource(require('assets/apple.svg')); function fromSource(source: ImageSource) { - return (props: ImageProps & { size?: number }) => ( - + return ({ + onPress, + ...props + }: ImageProps & { size?: number } & Pick) => ( + + + ); } diff --git a/app/src/util/theme/navigation.ts b/app/src/util/theme/navigation.ts index c42e50402..dc4398078 100644 --- a/app/src/util/theme/navigation.ts +++ b/app/src/util/theme/navigation.ts @@ -7,5 +7,6 @@ export const NAVIGATION_THEME = { colors: { ...NavTheme.colors, ...PAPER_THEME.colors, + background: 'transparent', }, }; diff --git a/app/src/util/theme/paper.ts b/app/src/util/theme/paper.ts index 3a4ff3614..fe5deb451 100644 --- a/app/src/util/theme/paper.ts +++ b/app/src/util/theme/paper.ts @@ -1,7 +1,13 @@ -import { MD3LightTheme as overrided, useTheme as baseUseTheme } from 'react-native-paper'; +import { + MD3LightTheme as overrided, + useTheme as baseUseTheme, + configureFonts, +} from 'react-native-paper'; import color from 'color'; import { match } from 'ts-pattern'; import { Palette } from './palette'; +import { MD3Type } from 'react-native-paper/lib/typescript/types'; +import { FONT_BY_WEIGHT } from '~/components/Fonts'; const c = (c: string, f: (color: color) => color) => f(color(c)).hexa(); @@ -75,6 +81,20 @@ export const PAPER_THEME = { xl: 28, full: 1000, } as const, + + // Android doesn't support different font variants based on weight like web; so we change the family name + fonts: configureFonts({ + isV3: true, + config: Object.fromEntries( + Object.entries(overrided.fonts).map(([variant, properties]): [string, Partial] => { + const fontFamily = FONT_BY_WEIGHT[properties.fontWeight ?? '400']; + if (__DEV__ && !fontFamily) + throw new Error(`No font family configured for weight: ${properties.fontWeight}`); + + return [variant, { ...(fontFamily && { fontFamily }) }]; + }), + ), + }), }; export const ICON_SIZE = PAPER_THEME.iconSize; diff --git a/yarn.lock b/yarn.lock index f6033e33f..54887d721 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5415,9 +5415,9 @@ __metadata: languageName: node linkType: hard -"@expo/cli@npm:0.10.13": - version: 0.10.13 - resolution: "@expo/cli@npm:0.10.13" +"@expo/cli@npm:0.10.14": + version: 0.10.14 + resolution: "@expo/cli@npm:0.10.14" dependencies: "@babel/runtime": ^7.20.0 "@expo/code-signing-certificates": 0.0.5 @@ -5483,7 +5483,7 @@ __metadata: ws: ^8.12.1 bin: expo-internal: build/bin/cli - checksum: 3dda76f39a9658317bd80715fe075e8f6d3e09c0ca2ce38b5042f72c2ac7d5c8d2229beed614104d396a1dd1d40d09755707e671a8dab6fbdc9d9b5dd9d060e2 + checksum: 88bda58114c47fb9f6371a9e688ff12fffac8c7ffc6e29baf1f0f9ab99ac4ed0595f332328e179319f6fbd5e08cbcdd169453e18a673d4032a29e6f1d1594c7c languageName: node linkType: hard @@ -14164,7 +14164,7 @@ __metadata: eslint-plugin-react: ^7.33.2 eth-url-parser: ^1.0.4 ethers: ^5.7.2 - expo: ^49.0.13 + expo: ^49.0.16 expo-apple-authentication: ~6.1.0 expo-application: ~5.3.0 expo-asset: ~8.10.1 @@ -14176,6 +14176,7 @@ __metadata: expo-constants: ~14.4.2 expo-dev-client: ~2.4.11 expo-device: ~5.4.0 + expo-font: ~11.6.0 expo-haptics: ~12.4.0 expo-image: ~1.3.4 expo-keep-awake: ~12.3.0 @@ -21371,6 +21372,17 @@ __metadata: languageName: node linkType: hard +"expo-font@npm:~11.6.0": + version: 11.6.0 + resolution: "expo-font@npm:11.6.0" + dependencies: + fontfaceobserver: ^2.1.0 + peerDependencies: + expo: "*" + checksum: 0260af20458daf36f0de27097639432ed456c95cf6116f1e5b092eec0cca0e9fb5ad67811fce0505c66fcbb63c8e529857fdcb3ff90c3d6a4f585545cf80e76b + languageName: node + linkType: hard + "expo-haptics@npm:~12.4.0": version: 12.4.0 resolution: "expo-haptics@npm:12.4.0" @@ -21688,12 +21700,12 @@ __metadata: languageName: node linkType: hard -"expo@npm:^49.0.13": - version: 49.0.14 - resolution: "expo@npm:49.0.14" +"expo@npm:^49.0.16": + version: 49.0.16 + resolution: "expo@npm:49.0.16" dependencies: "@babel/runtime": ^7.20.0 - "@expo/cli": 0.10.13 + "@expo/cli": 0.10.14 "@expo/config": 8.1.2 "@expo/config-plugins": 7.2.5 "@expo/vector-icons": ^13.0.0 @@ -21714,7 +21726,7 @@ __metadata: uuid: ^3.4.0 bin: expo: bin/cli - checksum: 56a1cb91b5cfaaa3e2ccf722252746af50bcfd972ed45a708fd177efc08b2b2449803565683524b60a2e7b546dd6bd660516b1cf8b035566b065202b2f388c40 + checksum: 5f65fd86a3a3bd13f8d186bcdedc80f3261b6db4c0605a6985c6bb16e2f4e2d50052daf93d6d9fce5892fbb598b2ab4f916d57d2b02064c0cc7807e93795acbf languageName: node linkType: hard