Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create profile screen #17

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
66841fb
Make Avatar size prop; use it in TabScreenHeader, and make it a link
jessepinho Jan 2, 2025
bb59a20
Ensure button label is always one line
jessepinho Jan 2, 2025
26d840d
Create ListItemChevronRightSuffix; use it in DepositMethod (and fix a…
jessepinho Jan 2, 2025
2e57efc
Build out profile and default payment token screens
jessepinho Jan 2, 2025
9606c65
Install/configure expo-sqlite
jessepinho Jan 2, 2025
c5d05be
Revert "Install/configure expo-sqlite"
jessepinho Jan 2, 2025
178cf60
Install/configure expo-secure-store
jessepinho Jan 2, 2025
a5a2f1e
Add secureStore key to Redux
jessepinho Jan 3, 2025
d637ce2
Create ListItemIconSuffix
jessepinho Jan 3, 2025
0f9b83b
Hook up the screen to Redux
jessepinho Jan 3, 2025
8ff9c12
Add persistor to the app
jessepinho Jan 3, 2025
917064d
Create separate root reducer for Storybook etc.
jessepinho Jan 3, 2025
1d1995e
Add token search functionality
jessepinho Jan 3, 2025
4c1c025
Add startAdornment prop; add stories
jessepinho Jan 3, 2025
681c42a
Create helper component
jessepinho Jan 3, 2025
c99198e
Add placeholder and clearButtonMode props to TextInput
jessepinho Jan 3, 2025
e8c0eed
Make text input on default payment token nicer
jessepinho Jan 3, 2025
a8579ac
Add keyboardType prop
jessepinho Jan 3, 2025
21dca8c
Add more docs re: secureStore
jessepinho Jan 3, 2025
28f7a46
Create Box component
jessepinho Jan 3, 2025
f95e4ca
Show correct secondary text
jessepinho Jan 3, 2025
43a1171
Build out GRPC screen; fix some issues
jessepinho Jan 3, 2025
5872fbc
Document the store
jessepinho Jan 3, 2025
790ddea
More docs
jessepinho Jan 3, 2025
81af19f
Comments
jessepinho Jan 3, 2025
74e5b3f
Load gRPC endpoints properly via a Thunk
jessepinho Jan 3, 2025
fd3a152
Make string translatable
jessepinho Jan 3, 2025
f2e2e74
yarn extract/compile
jessepinho Jan 3, 2025
b53d703
Comments
jessepinho Jan 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions react-native-expo/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import { configureStore } from '@reduxjs/toolkit';
import { DripsyProvider } from 'dripsy';
import dripsyTheme from '../utils/dripsyTheme';
import PraxI18nProvider from '../components/PraxI18nProvider';
import rootReducer from '../store/rootReducer';
import type { Preview } from '@storybook/react';
import React from 'react';
import ReduxProvider from '../components/ReduxProvider';

/**
* Ideally, we'd use `<FontProvider />` in the root decorator to provide fonts
* to Storybook. But that caused weird import issues. So for now, we'll just add
* an `@font-face` CSS file to make our fonts work.
*/
import './fonts.css';
import { Provider } from 'react-redux';

/**
* For Storybook, we use a simplified version of the Redux store which doesn't
* include e.g., `redux-persist`.
*/
const store = configureStore({ reducer: rootReducer });

const preview: Preview = {
decorators: [
Story => (
<ReduxProvider>
<PraxI18nProvider>
<PraxI18nProvider>
<Provider store={store}>
<DripsyProvider theme={dripsyTheme}>
<Story />
</DripsyProvider>
</PraxI18nProvider>
</ReduxProvider>
</Provider>
</PraxI18nProvider>
),
],
parameters: {
Expand Down
10 changes: 10 additions & 0 deletions react-native-expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,13 @@ We use [Redux](https://redux.js.org/) for state management in Prax Mobile, along
### Typed hooks

To use `redux-react`'s `useSelector` and `useDispatch` hooks, do not import them directly. Instead, import the typed versions from `@/store/hooks`, which are bound to our store's TypeScript types.

### The `secureStore` slice

You can use the `secureStore` slice in Redux like any other state slice, but note that all data in `secureStore` will be persisted to encrypted on-device storage, which persists between device and app restarts. So, only store data in that slice that is meant to be persisted, such as private keys, user preferences, etc.

Note that we use secure storage regardless of the sensitivity of the data. So, both the user's full viewing key and the user's default payment token are stored in encrypted storage.

If you're developing a screen that sets data in secure storage but also sets temporary state data that shouldn't be persisted, the pattern we use is to create a separate slice for that screen that only contains the temporary data. For example, `<DefaultPaymentTokenScreen />` uses the `defaultPaymentTokenScreen` slice to maintain its screen state (such as, for example, the contents of the text field on the screen) and the `secureStore` slice to store the user's preferred payment token.

The slice can be consumed and updated just like any other slice (via `useAppSelector()` and `useAppDispatch()`), and can have its shape modified just like any other slice (in `store/secureStore.ts`). The persistence is accomplished via [`redux-persist`](https://github.com/rt2zz/redux-persist) and [redux-persist-expo-securestore](https://github.com/Cretezy/redux-persist-expo-securestore), using [Expo SecureStore](https://docs.expo.dev/versions/latest/sdk/securestore/) under the hood. See `store/index.ts` to see how it's configured.
8 changes: 6 additions & 2 deletions react-native-expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.margulus.reactnativeexpo"
"bundleIdentifier": "com.margulus.reactnativeexpo",
"config": {
"usesNonExemptEncryption": false
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See https://docs.expo.dev/versions/latest/sdk/securestore/#exempting-encryption-prompt

(I can't leave this comment in the file, since JSON doesn't support comments 😭 )

}
},
"android": {
"adaptiveIcon": {
Expand All @@ -33,7 +36,8 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
]
],
"expo-secure-store"
],
"experiments": {
"typedRoutes": true
Expand Down
5 changes: 5 additions & 0 deletions react-native-expo/app/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ProfileScreen from '@/components/ProfileScreen';

export default function ProfileRoute() {
return <ProfileScreen />;
}
5 changes: 5 additions & 0 deletions react-native-expo/app/profile/defaultPaymentToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DefaultPaymentTokenScreen from '@/components/ProfileScreen/DefaultPaymentTokenScreen';

export default function DefaultPaymentTokenRoute() {
return <DefaultPaymentTokenScreen />;
}
5 changes: 5 additions & 0 deletions react-native-expo/app/profile/grpc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import GrpcScreen from '@/components/ProfileScreen/GrpcScreen';

export default function GrpcRoute() {
return <GrpcScreen />;
}
1 change: 1 addition & 0 deletions react-native-expo/components/Avatar/index.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default meta;
export const Primary: StoryObj<typeof Avatar> = {
args: {
username: 'henry',
size: 'sm',
},
};
14 changes: 10 additions & 4 deletions react-native-expo/components/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ import { Sx, Text, View } from 'dripsy';

export interface AvatarProps {
username?: string;
/**
* - `sm`: 32px (for the tab screen headers)
* - `lg`: 80px (for the profile screen header)
*/
size: 'sm' | 'lg';
}

export default function Avatar({ username }: AvatarProps) {
export default function Avatar({ username, size }: AvatarProps) {
return (
<View sx={sx.root}>
<Text sx={sx.text}>{username ? username.substring(0, 1) : '?'}</Text>
<View sx={{ ...sx.root, size: size === 'sm' ? 32 : 80 }}>
<Text sx={{ ...sx.text, variant: size === 'lg' ? 'text.h4' : undefined }}>
{username ? username.substring(0, 1) : '?'}
</Text>
</View>
);
}
Expand All @@ -17,7 +24,6 @@ const sx = {
backgroundColor: 'neutralLight',
borderRadius: '50%',

size: '$8',
justifyContent: 'center',
alignItems: 'center',
},
Expand Down
21 changes: 21 additions & 0 deletions react-native-expo/components/Box/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meta, StoryObj } from '@storybook/react';

import Box from '.';
import { Text } from 'dripsy';

const meta: Meta<typeof Box> = {
component: Box,
tags: ['autodocs'],
argTypes: {
children: { control: false },
},
};

export default meta;

export const Basic: StoryObj<typeof Box> = {
args: {
padding: true,
children: <Text>Box content goes here</Text>,
},
};
31 changes: 31 additions & 0 deletions react-native-expo/components/Box/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Sx, View } from 'dripsy';
import { ReactNode } from 'react';

export interface BoxProps {
children?: ReactNode;
/**
* When `true`, provides default padding. Leave undefined (or `false`) when
* your children have their own padding -- e.g., in the case of `<ListItem
* />`s.
*/
padding?: boolean;
}

/**
* A simple, generic wrapper that provides a background and optional padding for
* its children.
*/
export default function Box({ children, padding }: BoxProps) {
return <View sx={{ ...sx.root, ...(padding ? sx.rootPadding : {}) }}>{children}</View>;
}

const sx = {
root: {
backgroundColor: 'neutralContrast',
borderRadius: 'lg',
},

rootPadding: {
p: '$4',
},
} satisfies Record<string, Sx>;
6 changes: 5 additions & 1 deletion react-native-expo/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export default function Button({
onPress={onPress}
disabled={disabled}
>
<Text variant='button' sx={actionType === 'accent' ? sx.textAccent : sx.textDefault}>
<Text
variant='button'
sx={actionType === 'accent' ? sx.textAccent : sx.textDefault}
numberOfLines={1}
>
{children}
</Text>
</Pressable>
Expand Down
9 changes: 4 additions & 5 deletions react-native-expo/components/DepositFlow/DepositMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { Text } from 'dripsy';
import ListItems from '../ListItems';
import ListItem from '../ListItem';
import AssetIcon from '../AssetIcon';
import Icon from '../Icon';
import { ChevronRight } from 'lucide-react-native';
import { setStep } from '@/store/depositFlow';
import { DepositFlowStep, setStep } from '@/store/depositFlow';
import { useAppDispatch } from '@/store/hooks';
import { Trans, useLingui } from '@lingui/react/macro';
import ListItemChevronRightSuffix from '../ListItemChevronRightSuffix';

export default function DepositMethod() {
const dispatch = useAppDispatch();
Expand All @@ -22,8 +21,8 @@ export default function DepositMethod() {
<ListItem
avatar={<AssetIcon />}
primaryText={t`Shielded IBC deposit`}
suffix={<Icon IconComponent={ChevronRight} size='md' color='neutralLight' />}
onPress={() => dispatch(setStep('address'))}
suffix={<ListItemChevronRightSuffix />}
onPress={() => dispatch(setStep(DepositFlowStep.Address))}
/>

<ListItem
Expand Down
27 changes: 13 additions & 14 deletions react-native-expo/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Sx, Text, View } from 'dripsy';
import { ReactNode } from 'react';
import Box from '../Box';

export interface ListProps {
/**
Expand All @@ -19,25 +20,23 @@ export interface ListProps {
*/
export default function List({ children, title, primaryAction }: ListProps) {
return (
<View sx={sx.root}>
{!!title && (
<View sx={sx.title}>
<Text variant='large'>{title}</Text>

{primaryAction}
</View>
)}
{children}
</View>
<Box>
<View sx={sx.root}>
{!!title && (
<View sx={sx.title}>
<Text variant='large'>{title}</Text>

{primaryAction}
</View>
)}
{children}
</View>
</Box>
);
}

const sx = {
root: {
backgroundColor: 'neutralContrast',

borderRadius: 'lg',

flexDirection: 'column',
},

Expand Down
12 changes: 12 additions & 0 deletions react-native-expo/components/ListItemChevronRightSuffix/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ChevronRight } from 'lucide-react-native';
import ListItemIconSuffix from '../ListItemIconSuffix';

/**
* Many `<ListItem />`s have a right-pointing chevron to indicate they can be
* tapped to open a new screen. Since this is such a common use case,
* `<ListItemChevronRightSuffix />` exists to be passed to the `<ListItem />`
* `suffix` prop.
*/
export default function ListItemChevronRightSuffix() {
return <ListItemIconSuffix IconComponent={ChevronRight} />;
}
15 changes: 15 additions & 0 deletions react-native-expo/components/ListItemIconSuffix/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LucideIcon } from 'lucide-react-native';
import Icon from '../Icon';

export interface ListItemIconSuffixProps {
IconComponent: LucideIcon;
}

/**
* Many `<ListItem />`s have an icon suffix. Since this is such a common use
* case, `<ListItemIconSuffix />` exists to be passed to the `<ListItem />`
* `suffix` prop.
*/
export default function ListItemIconSuffix({ IconComponent }: ListItemIconSuffixProps) {
return <Icon IconComponent={IconComponent} size='md' color='neutralLight' />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Asset from '@/types/Asset';

const ASSETS: Asset[] = [
{ name: 'USD Coin', symbol: 'USDC' },
{ name: 'Penumbra', symbol: 'UM' },
{ name: 'Cosmo', symbol: 'ATOM' },
{ name: 'Ethereum', symbol: 'ETH' },
{ name: 'Osmosis', symbol: 'OSMO' },
];

export default ASSETS;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import AssetIcon from '@/components/AssetIcon';
import List from '@/components/List';
import ListItem from '@/components/ListItem';
import ListItemIconSuffix from '@/components/ListItemIconSuffix';
import TextInput from '@/components/TextInput';
import { setSearchText } from '@/store/defaultPaymentTokenScreen';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setDefaultPaymentToken } from '@/store/secureStore';
import { Trans, useLingui } from '@lingui/react/macro';
import { Sx, Text, View } from 'dripsy';
import { Check, Search } from 'lucide-react-native';
import useFilteredAssets from './useFilteredAssets';
import TextInputIconStartAdornment from '@/components/TextInputIconStartAdornment';

export default function DefaultPaymentTokenScreen() {
const defaultPaymentToken = useAppSelector(state => state.secureStore.defaultPaymentToken);
const dispatch = useAppDispatch();
const searchText = useAppSelector(state => state.defaultPaymentTokenScreen.searchText);
const filteredAssets = useFilteredAssets();
const { t } = useLingui();

return (
<View sx={sx.root}>
<Text variant='h4'>
<Trans>Default payment token</Trans>
</Text>

<TextInput
value={searchText}
onChangeText={text => dispatch(setSearchText(text))}
startAdornment={<TextInputIconStartAdornment IconComponent={Search} />}
placeholder={t`Search tokens...`}
clearButtonMode='always'
/>

<List>
{filteredAssets.map(asset => (
<ListItem
key={asset.symbol}
avatar={<AssetIcon />}
primaryText={asset.symbol}
secondaryText={asset.name}
onPress={() => dispatch(setDefaultPaymentToken(asset.symbol))}
suffix={
asset.symbol === defaultPaymentToken ? (
<ListItemIconSuffix IconComponent={Check} />
) : undefined
}
/>
))}
</List>
</View>
);
}

const sx = {
root: {
px: 'screenHorizontalMargin',
flexDirection: 'column',
gap: '$4',
},
} satisfies Record<string, Sx>;
Loading