From 1555a2d2ef1506093e013145c1c5d2cf64ef6087 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Thu, 6 Jul 2023 23:39:10 +0200 Subject: [PATCH] Minibits source code - alpha version --- App.tsx | 118 -- index.js | 19 +- src/App.tsx | 51 + src/components/AutoImage.tsx | 72 ++ src/components/BottomModal.tsx | 235 ++++ src/components/Button.tsx | 190 +++ src/components/Card.tsx | 299 +++++ src/components/EmptyState.tsx | 219 ++++ src/components/ErrorModal.tsx | 51 + src/components/Header.tsx | 341 ++++++ src/components/Icon.tsx | 205 ++++ src/components/InfoModal.tsx | 44 + src/components/ListItem.tsx | 268 +++++ src/components/Loading.tsx | 18 + src/components/Screen.tsx | 236 ++++ src/components/Text.tsx | 112 ++ src/components/TextField.tsx | 275 +++++ src/components/Toggle.tsx | 655 +++++++++++ src/components/index.ts | 15 + src/config/config.base.ts | 26 + src/config/config.dev.ts | 10 + src/config/config.prod.ts | 10 + src/config/index.ts | 28 + src/i18n/en.ts | 152 +++ src/i18n/i18n.ts | 44 + src/i18n/index.ts | 3 + src/i18n/translate.ts | 32 + src/models/Invoice.ts | 53 + src/models/InvoicesStore.ts | 76 ++ src/models/Mint.ts | 63 + src/models/MintsStore.ts | 119 ++ src/models/Proof.ts | 40 + src/models/ProofsStore.ts | 198 ++++ src/models/RootStore.ts | 36 + src/models/Token.ts | 13 + src/models/TokenEntry.ts | 24 + src/models/Transaction.ts | 66 ++ src/models/TransactionsStore.ts | 248 ++++ src/models/UserSettingsStore.ts | 79 ++ src/models/helpers/getRootStore.ts | 10 + src/models/helpers/setupRootStore.ts | 173 +++ src/models/helpers/useStores.ts | 83 ++ src/models/helpers/withSetPropAction.ts | 30 + src/models/index.ts | 4 + src/navigation/AppNavigator.tsx | 93 ++ src/navigation/TabsNavigator.tsx | 189 +++ src/navigation/index.ts | 4 + src/navigation/navigationUtilities.ts | 171 +++ src/screens/BackupScreen.tsx | 228 ++++ src/screens/ContactsScreen.tsx | 27 + src/screens/DeveloperScreen.tsx | 197 ++++ src/screens/ErrorScreen/ErrorBoundary.tsx | 70 ++ src/screens/ErrorScreen/ErrorDetails.tsx | 87 ++ src/screens/LocalRecoveryScreen.tsx | 402 +++++++ src/screens/Mints/MintListItem.tsx | 45 + src/screens/MintsScreen.tsx | 452 +++++++ src/screens/ReceiveScreen.tsx | 464 ++++++++ src/screens/ScanScreen.tsx | 207 ++++ src/screens/SecurityScreen.tsx | 174 +++ src/screens/SendScreen.tsx | 807 +++++++++++++ src/screens/SettingsScreen.tsx | 154 +++ src/screens/TopupScreen.tsx | 766 ++++++++++++ src/screens/TranDetailScreen.tsx | 932 +++++++++++++++ src/screens/TranHistoryScreen.tsx | 242 ++++ .../Transactions/TransactionListItem.tsx | 179 +++ src/screens/TransferScreen.tsx | 590 ++++++++++ src/screens/Wallet/ResultModalInfo.tsx | 34 + src/screens/WalletScreen.tsx | 513 ++++++++ src/screens/WelcomeScreen.tsx | 132 +++ src/screens/index.ts | 22 + src/services/asyncStorage.ts | 88 ++ src/services/cashuHelpers.ts | 311 +++++ src/services/cashuMintClient.ts | 267 +++++ src/services/index.ts | 6 + src/services/keyChain.ts | 153 +++ src/services/mmkvStorage.ts | 221 ++++ src/services/sqlite.ts | 819 +++++++++++++ src/services/storage.test.ts | 39 + src/services/walletService.ts | 1035 +++++++++++++++++ src/theme/colors.ts | 216 ++++ src/theme/index.ts | 5 + src/theme/spacing.ts | 20 + src/theme/timing.ts | 6 + src/theme/typography.ts | 54 + src/theme/useThemeColor.tsx | 20 + src/utils/AppError.ts | 59 + src/utils/dateUtils.ts | 25 + src/utils/delay.ts | 7 + src/utils/envtypes/env.d.tsx | 7 + src/utils/eventEmitter.ts | 51 + src/utils/ignoreWarnings.ts | 10 + src/utils/logger.ts | 128 ++ src/utils/number.ts | 28 + src/utils/openLinkInBrowser.ts | 8 + src/utils/poller.ts | 32 + src/utils/useHeader.tsx | 20 + src/utils/useIsMounted.ts | 15 + src/utils/useSafeAreaInsetsStyle.ts | 47 + 98 files changed, 15796 insertions(+), 125 deletions(-) delete mode 100644 App.tsx create mode 100644 src/App.tsx create mode 100644 src/components/AutoImage.tsx create mode 100644 src/components/BottomModal.tsx create mode 100644 src/components/Button.tsx create mode 100644 src/components/Card.tsx create mode 100644 src/components/EmptyState.tsx create mode 100644 src/components/ErrorModal.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/Icon.tsx create mode 100644 src/components/InfoModal.tsx create mode 100644 src/components/ListItem.tsx create mode 100644 src/components/Loading.tsx create mode 100644 src/components/Screen.tsx create mode 100644 src/components/Text.tsx create mode 100644 src/components/TextField.tsx create mode 100644 src/components/Toggle.tsx create mode 100644 src/components/index.ts create mode 100644 src/config/config.base.ts create mode 100644 src/config/config.dev.ts create mode 100644 src/config/config.prod.ts create mode 100644 src/config/index.ts create mode 100644 src/i18n/en.ts create mode 100644 src/i18n/i18n.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/translate.ts create mode 100644 src/models/Invoice.ts create mode 100644 src/models/InvoicesStore.ts create mode 100644 src/models/Mint.ts create mode 100644 src/models/MintsStore.ts create mode 100644 src/models/Proof.ts create mode 100644 src/models/ProofsStore.ts create mode 100644 src/models/RootStore.ts create mode 100644 src/models/Token.ts create mode 100644 src/models/TokenEntry.ts create mode 100644 src/models/Transaction.ts create mode 100644 src/models/TransactionsStore.ts create mode 100644 src/models/UserSettingsStore.ts create mode 100644 src/models/helpers/getRootStore.ts create mode 100644 src/models/helpers/setupRootStore.ts create mode 100644 src/models/helpers/useStores.ts create mode 100644 src/models/helpers/withSetPropAction.ts create mode 100644 src/models/index.ts create mode 100644 src/navigation/AppNavigator.tsx create mode 100644 src/navigation/TabsNavigator.tsx create mode 100644 src/navigation/index.ts create mode 100644 src/navigation/navigationUtilities.ts create mode 100644 src/screens/BackupScreen.tsx create mode 100644 src/screens/ContactsScreen.tsx create mode 100644 src/screens/DeveloperScreen.tsx create mode 100644 src/screens/ErrorScreen/ErrorBoundary.tsx create mode 100644 src/screens/ErrorScreen/ErrorDetails.tsx create mode 100644 src/screens/LocalRecoveryScreen.tsx create mode 100644 src/screens/Mints/MintListItem.tsx create mode 100644 src/screens/MintsScreen.tsx create mode 100644 src/screens/ReceiveScreen.tsx create mode 100644 src/screens/ScanScreen.tsx create mode 100644 src/screens/SecurityScreen.tsx create mode 100644 src/screens/SendScreen.tsx create mode 100644 src/screens/SettingsScreen.tsx create mode 100644 src/screens/TopupScreen.tsx create mode 100644 src/screens/TranDetailScreen.tsx create mode 100644 src/screens/TranHistoryScreen.tsx create mode 100644 src/screens/Transactions/TransactionListItem.tsx create mode 100644 src/screens/TransferScreen.tsx create mode 100644 src/screens/Wallet/ResultModalInfo.tsx create mode 100644 src/screens/WalletScreen.tsx create mode 100644 src/screens/WelcomeScreen.tsx create mode 100644 src/screens/index.ts create mode 100644 src/services/asyncStorage.ts create mode 100644 src/services/cashuHelpers.ts create mode 100644 src/services/cashuMintClient.ts create mode 100644 src/services/index.ts create mode 100644 src/services/keyChain.ts create mode 100644 src/services/mmkvStorage.ts create mode 100644 src/services/sqlite.ts create mode 100644 src/services/storage.test.ts create mode 100644 src/services/walletService.ts create mode 100644 src/theme/colors.ts create mode 100644 src/theme/index.ts create mode 100644 src/theme/spacing.ts create mode 100644 src/theme/timing.ts create mode 100644 src/theme/typography.ts create mode 100644 src/theme/useThemeColor.tsx create mode 100644 src/utils/AppError.ts create mode 100644 src/utils/dateUtils.ts create mode 100644 src/utils/delay.ts create mode 100644 src/utils/envtypes/env.d.tsx create mode 100644 src/utils/eventEmitter.ts create mode 100644 src/utils/ignoreWarnings.ts create mode 100644 src/utils/logger.ts create mode 100644 src/utils/number.ts create mode 100644 src/utils/openLinkInBrowser.ts create mode 100644 src/utils/poller.ts create mode 100644 src/utils/useHeader.tsx create mode 100644 src/utils/useIsMounted.ts create mode 100644 src/utils/useSafeAreaInsetsStyle.ts diff --git a/App.tsx b/App.tsx deleted file mode 100644 index bf24c331..00000000 --- a/App.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Sample React Native App - * https://github.com/facebook/react-native - * - * @format - */ - -import React from 'react'; -import type {PropsWithChildren} from 'react'; -import { - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - useColorScheme, - View, -} from 'react-native'; - -import { - Colors, - DebugInstructions, - Header, - LearnMoreLinks, - ReloadInstructions, -} from 'react-native/Libraries/NewAppScreen'; - -type SectionProps = PropsWithChildren<{ - title: string; -}>; - -function Section({children, title}: SectionProps): JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; - return ( - - - {title} - - - {children} - - - ); -} - -function App(): JSX.Element { - const isDarkMode = useColorScheme() === 'dark'; - - const backgroundStyle = { - backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, - }; - - return ( - - - -
- -
- Edit App.tsx to change this - screen and then come back to see your edits. -
-
- -
-
- -
-
- Read the docs to discover what to do next: -
- -
- - - ); -} - -const styles = StyleSheet.create({ - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', - }, - highlight: { - fontWeight: '700', - }, -}); - -export default App; diff --git a/index.js b/index.js index a850d031..a701d0b1 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,14 @@ -/** - * @format - */ +import 'react-native-reanimated' // needed for qrcode reader +import Crypto from 'react-native-quick-crypto' // needed for secp256k1, conf in babel.config +import 'react-native-url-polyfill/auto' // URL.host etc +import 'text-encoding-polyfill' // needed in cashu-ts +import {AppRegistry} from 'react-native' +import App from './src/App' +import {name as appName} from './app.json' -import {AppRegistry} from 'react-native'; -import App from './App'; -import {name as appName} from './app.json'; -AppRegistry.registerComponent(appName, () => App); +function BootstrapApp() { + return +} + +AppRegistry.registerComponent(appName, () => BootstrapApp) diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..2f5dc2ed --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import * as Sentry from '@sentry/react-native' +import {SENTRY_DSN} from '@env' +import { + initialWindowMetrics, + SafeAreaProvider, +} from 'react-native-safe-area-context' + +import {AppNavigator} from './navigation' +import {useInitialRootStore} from './models' +import {Database} from './services' +import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary' +import Config from './config' +import {log} from './utils/logger' + +Sentry.init({ + dsn: SENTRY_DSN, +}) + +interface AppProps { + appName: string +} + +function App(props: AppProps) { + // const isDarkMode = useColorScheme() === 'dark' + log.info(`${props.appName} app started...`) + + const {rehydrated} = useInitialRootStore(() => { + // This runs after the root store has been initialized and rehydrated from storage. + log.trace('Root store rehydrated', [], 'useInitialRootStore') + + // This creates and opens a sqlite database that stores transactions history. + // It triggers db migrations if database version has changed. + // As it runs inside a callback, it should not block UI. + const dbVersion = Database.getDatabaseVersion() + }) + + if (!rehydrated) { + return null + } + + return ( + + + + + + ) +} + +export default App diff --git a/src/components/AutoImage.tsx b/src/components/AutoImage.tsx new file mode 100644 index 00000000..10eb0da9 --- /dev/null +++ b/src/components/AutoImage.tsx @@ -0,0 +1,72 @@ +import React, { useLayoutEffect, useState } from "react" +import { Image, ImageProps, ImageURISource, Platform } from "react-native" + +// TODO: document new props +export interface AutoImageProps extends ImageProps { + /** + * How wide should the image be? + */ + maxWidth?: number + /** + * How tall should the image be? + */ + maxHeight?: number +} + +/** + * A hook that will return the scaled dimensions of an image based on the + * provided dimensions' aspect ratio. If no desired dimensions are provided, + * it will return the original dimensions of the remote image. + * + * How is this different from `resizeMode: 'contain'`? Firstly, you can + * specify only one side's size (not both). Secondly, the image will scale to fit + * the desired dimensions instead of just being contained within its image-container. + * + */ +export function useAutoImage( + remoteUri: string, + dimensions?: [maxWidth: number, maxHeight: number], +): [width: number, height: number] { + const [[remoteWidth, remoteHeight], setRemoteImageDimensions] = useState([0, 0]) + const remoteAspectRatio = remoteWidth / remoteHeight + const [maxWidth, maxHeight] = dimensions ?? [] + + useLayoutEffect(() => { + if (!remoteUri) return + + Image.getSize(remoteUri, (w, h) => setRemoteImageDimensions([w, h])) + }, [remoteUri]) + + if (Number.isNaN(remoteAspectRatio)) return [0, 0] + + if (maxWidth && maxHeight) { + const aspectRatio = Math.min(maxWidth / remoteWidth, maxHeight / remoteHeight) + return [remoteWidth * aspectRatio, remoteHeight * aspectRatio] + } else if (maxWidth) { + return [maxWidth, maxWidth / remoteAspectRatio] + } else if (maxHeight) { + return [maxHeight * remoteAspectRatio, maxHeight] + } else { + return [remoteWidth, remoteHeight] + } +} + +/** + * An Image component that automatically sizes a remote or data-uri image. + * + * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-AutoImage.md) + */ +export function AutoImage(props: AutoImageProps) { + const { maxWidth, maxHeight, ...ImageProps } = props + const source = props.source as ImageURISource + + const [width, height] = useAutoImage( + Platform.select({ + web: (source?.uri as string) ?? (source as string), + default: source?.uri as string, + }), + [maxWidth, maxHeight], + ) + + return +} diff --git a/src/components/BottomModal.tsx b/src/components/BottomModal.tsx new file mode 100644 index 00000000..943f36da --- /dev/null +++ b/src/components/BottomModal.tsx @@ -0,0 +1,235 @@ +import React, { ComponentType, Fragment, ReactElement } from "react" +import Modal from 'react-native-modal' +import { + StyleProp, + TextStyle, + ViewProps, + View, + ViewStyle, + ColorValue, +} from "react-native" +import { colors, useThemeColor, spacing } from "../theme" +import { Text, TextProps } from "./Text" + + +interface ModalProps extends ViewProps { + isVisible?: boolean + top?: number + onBackdropPress?: any + onBackButtonPress?: any + backdropOpacity?: number + /** + * The heading text to display if not using `headingTx`. + */ + heading?: TextProps["text"] + /** + * Heading text which is looked up via i18n. + */ + headingTx?: TextProps["tx"] + /** + * Optional heading options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + headingTxOptions?: TextProps["txOptions"] + /** + * Style overrides for heading text. + */ + headingStyle?: StyleProp + /** + * Pass any additional props directly to the heading Text component. + */ + HeadingTextProps?: TextProps + /** + * Custom heading component. + * Overrides all other `heading*` props. + */ + HeadingComponent?: ReactElement + /** + * The content text to display if not using `contentTx`. + */ + content?: TextProps["text"] + /** + * Content text which is looked up via i18n. + */ + contentTx?: TextProps["tx"] + /** + * Optional content options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + contentTxOptions?: TextProps["txOptions"] + /** + * Style overrides for content text. + */ + contentStyle?: StyleProp + /** + * Pass any additional props directly to the content Text component. + */ + ContentTextProps?: TextProps + /** + * Custom content component. + * Overrides all other `content*` props. + */ + ContentComponent?: ReactElement + /** + * The footer text to display if not using `footerTx`. + */ + footer?: TextProps["text"] + /** + * Footer text which is looked up via i18n. + */ + footerTx?: TextProps["tx"] + /** + * Optional footer options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + footerTxOptions?: TextProps["txOptions"] + /** + * Style overrides for footer text. + */ + footerStyle?: StyleProp + /** + * Pass any additional props directly to the footer Text component. + */ + FooterTextProps?: TextProps + /** + * Custom footer component. + * Overrides all other `footer*` props. + */ + FooterComponent?: ReactElement +} + +/** + * Modal + */ +export function BottomModal(props: ModalProps) { + + // TODO based on number of childs + const defaultTop = spacing.screenHeight * 0.8 + + const { + isVisible = true, + top = defaultTop, + onBackdropPress, + onBackButtonPress, + backdropOpacity = 0.05, + content, + contentTx, + contentTxOptions, + footer, + footerTx, + footerTxOptions, + heading, + headingTx, + headingTxOptions, + ContentComponent, + HeadingComponent, + FooterComponent, + style: $containerStyleOverride, + contentStyle: $contentStyleOverride, + headingStyle: $headingStyleOverride, + footerStyle: $footerStyleOverride, + ContentTextProps, + HeadingTextProps, + FooterTextProps, + ...otherProps + } = props + + const $innerContainerStyle = [ + $innerContainerBase, { backgroundColor: useThemeColor('card') }, $containerStyleOverride + ] + + const isHeadingPresent = !!(HeadingComponent || heading || headingTx) + const isContentPresent = !!(ContentComponent || content || contentTx) + const isFooterPresent = !!(FooterComponent || footer || footerTx) + + const $headingStyle = [ + (isFooterPresent || isContentPresent) && { marginBottom: spacing.micro }, + $headingStyleOverride, + HeadingTextProps?.style, + ] + const $contentStyle = [ + isHeadingPresent && { marginTop: spacing.micro }, + isFooterPresent && { marginBottom: spacing.micro }, + $contentStyleOverride, + ContentTextProps?.style, + ] + const $footerStyle = [ + (isHeadingPresent || isContentPresent) && { marginTop: spacing.micro }, + $footerStyleOverride, + FooterTextProps?.style, + ] + + + return ( + + + + {HeadingComponent || + (isHeadingPresent && ( + + ))} + + {ContentComponent || + (isContentPresent && ( + + ))} + + + {FooterComponent || + (isFooterPresent && ( + + ))} + + + ) +} + +const $outerContainerBase: ViewStyle = { + margin: 0, + bottom: 0, + borderTopLeftRadius: spacing.large, + borderTopRightRadius: spacing.large, + shadowColor: colors.palette.neutral900, + //shadowOffset: { width: 10, height: -50 }, + shadowOpacity: 1, + shadowRadius: 10, + elevation: 10, +} + +const $innerContainerBase: ViewStyle = { + flex: 1, + borderTopLeftRadius: spacing.large, + borderTopRightRadius: spacing.large, +} + + diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 00000000..63ab6a51 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,190 @@ +import React, { ComponentType } from "react" +import { + Pressable, + PressableProps, + PressableStateCallbackType, + StyleProp, + TextStyle, + ViewStyle, +} from "react-native" +import { colors, spacing, typography, useThemeColor } from "../theme" +import { Text, TextProps } from "./Text" + +type Presets = keyof typeof $viewPresets + +export interface ButtonAccessoryProps { + style: StyleProp + pressableState: PressableStateCallbackType +} + +export interface ButtonProps extends PressableProps { + /** + * Text which is looked up via i18n. + */ + tx?: TextProps["tx"] + /** + * The text to display if not using `tx` or nested components. + */ + text?: TextProps["text"] + /** + * Optional options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + txOptions?: TextProps["txOptions"] + /** + * An optional style override useful for padding & margin. + */ + style?: StyleProp + /** + * An optional style override for the "pressed" state. + */ + pressedStyle?: StyleProp + /** + * An optional style override for the button text. + */ + textStyle?: StyleProp + /** + * An optional style override for the button text when in the "pressed" state. + */ + pressedTextStyle?: StyleProp + /** + * One of the different types of button presets. + */ + preset?: Presets + /** + * An optional component to render on the right side of the text. + * Example: `RightAccessory={(props) => }` + */ + RightAccessory?: ComponentType + /** + * An optional component to render on the left side of the text. + * Example: `LeftAccessory={(props) => }` + */ + LeftAccessory?: ComponentType + /** + * Children components. + */ + children?: React.ReactNode +} + +/** + * A component that allows users to take actions and make choices. + * Wraps the Text component with a Pressable component. + * + * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Button.md) + */ +export function Button(props: ButtonProps) { + const { + tx, + text, + txOptions, + preset = "default", + style: $viewStyleOverride, + pressedStyle: $pressedViewStyleOverride, + textStyle: $textStyleOverride, + pressedTextStyle: $pressedTextStyleOverride, + children, + RightAccessory, + LeftAccessory, + ...rest + } = props + + const textColor = useThemeColor("text") + const defaultBg = useThemeColor("button") + const secondaryBg = useThemeColor("background") + const tertiaryBg = "transparent" + + function $viewStyle({ pressed }) { + return [ + $viewPresets[preset], + { backgroundColor: defaultBg }, + preset === "secondary" && { backgroundColor: secondaryBg }, + preset === "tertiary" && { backgroundColor: tertiaryBg }, + $viewStyleOverride, + !!pressed && [$pressedViewPresets[preset], $pressedViewStyleOverride], + ] + } + function $textStyle({ pressed }) { + return [ + $textPresets[preset], + { color: textColor }, + preset === "default" && { color: 'white' }, + // $textStyleOverride, + !!pressed && [$pressedTextPresets[preset], $pressedTextStyleOverride], + ] + } + + return ( + + {(state) => ( + <> + {!!LeftAccessory && } + + + {children} + + + {!!RightAccessory && ( + + )} + + )} + + ) +} + +const $baseViewStyle: ViewStyle = { + minHeight: 52, + borderRadius: 8, + justifyContent: "center", + alignItems: "center", + flexDirection: "row", + paddingVertical: spacing.small, + paddingHorizontal: spacing.small, + overflow: "hidden", +} + +const $baseTextStyle: TextStyle = { + fontSize: 16, + lineHeight: 20, + fontFamily: typography.primary?.light, + textAlign: "center", + flexShrink: 1, + flexGrow: 0, + zIndex: 2, +} + +const $rightAccessoryStyle: ViewStyle = { marginStart: spacing.extraSmall, zIndex: 1 } +const $leftAccessoryStyle: ViewStyle = { marginEnd: spacing.extraSmall, zIndex: 1 } + +const $viewPresets = { + default: [ + $baseViewStyle, + ] as StyleProp, + + secondary: [ + $baseViewStyle, + ] as StyleProp, + + tertiary: [ + $baseViewStyle, + ] as StyleProp, +} + +const $textPresets: Record> = { + default: $baseTextStyle, + secondary: $baseTextStyle, + tertiary: $baseTextStyle, +} + +const $pressedViewPresets: Record> = { + default: { backgroundColor: colors.palette.success100 }, + secondary: { backgroundColor: colors.palette.neutral500 }, + tertiary: { backgroundColor: colors.palette.neutral500 }, +} + +const $pressedTextPresets: Record> = { + default: { opacity: 0.9 }, + secondary: { opacity: 0.9 }, + tertiary: { opacity: 0.9 }, +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 00000000..918c5e29 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,299 @@ +import React, { ComponentType, Fragment, ReactElement } from "react" +import { + StyleProp, + TextStyle, + TouchableOpacity, + TouchableOpacityProps, + View, + ViewStyle, + useColorScheme +} from "react-native" +import { colors, useThemeColor, spacing } from "../theme" +import { Text, TextProps } from "./Text" + +type Presets = keyof typeof $containerPresets + +interface CardProps extends TouchableOpacityProps { + /** + * One of the different types of text presets. + */ + preset?: Presets + /** + * How the content should be aligned vertically. This is especially (but not exclusively) useful + * when the card is a fixed height but the content is dynamic. + * + * `top` (default) - aligns all content to the top. + * `center` - aligns all content to the center. + * `space-between` - spreads out the content evenly. + * `force-footer-bottom` - aligns all content to the top, but forces the footer to the bottom. + */ + verticalAlignment?: "top" | "center" | "space-between" | "force-footer-bottom" + /** + * Custom component added to the left of the card body. + */ + LeftComponent?: ReactElement + /** + * Custom component added to the right of the card body. + */ + RightComponent?: ReactElement + /** + * The heading text to display if not using `headingTx`. + */ + heading?: TextProps["text"] + /** + * Heading text which is looked up via i18n. + */ + headingTx?: TextProps["tx"] + /** + * Optional heading options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + headingTxOptions?: TextProps["txOptions"] + /** + * Style overrides for heading text. + */ + headingStyle?: StyleProp + /** + * Pass any additional props directly to the heading Text component. + */ + HeadingTextProps?: TextProps + /** + * Custom heading component. + * Overrides all other `heading*` props. + */ + HeadingComponent?: ReactElement + /** + * The content text to display if not using `contentTx`. + */ + content?: TextProps["text"] + /** + * Content text which is looked up via i18n. + */ + contentTx?: TextProps["tx"] + /** + * Optional content options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + contentTxOptions?: TextProps["txOptions"] + /** + * Style overrides for content text. + */ + contentStyle?: StyleProp + /** + * Pass any additional props directly to the content Text component. + */ + ContentTextProps?: TextProps + /** + * Custom content component. + * Overrides all other `content*` props. + */ + ContentComponent?: ReactElement + /** + * The footer text to display if not using `footerTx`. + */ + footer?: TextProps["text"] + /** + * Footer text which is looked up via i18n. + */ + footerTx?: TextProps["tx"] + /** + * Optional footer options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + footerTxOptions?: TextProps["txOptions"] + /** + * Style overrides for footer text. + */ + footerStyle?: StyleProp + /** + * Pass any additional props directly to the footer Text component. + */ + FooterTextProps?: TextProps + /** + * Custom footer component. + * Overrides all other `footer*` props. + */ + FooterComponent?: ReactElement +} + +/** + * Cards are useful for displaying related information in a contained way. + * If a ListItem displays content horizontally, a Card can be used to display content vertically. + * + * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-Card.md) + */ +export function Card(props: CardProps) { + + const backgroundColor = useThemeColor('card') + + const { + preset = "default", + content, + contentTx, + contentTxOptions, + footer, + footerTx, + footerTxOptions, + heading, + headingTx, + headingTxOptions, + ContentComponent, + HeadingComponent, + FooterComponent, + LeftComponent, + RightComponent, + verticalAlignment = "top", + style: $containerStyleOverride, + contentStyle: $contentStyleOverride, + headingStyle: $headingStyleOverride, + footerStyle: $footerStyleOverride, + ContentTextProps, + HeadingTextProps, + FooterTextProps, + ...WrapperProps + } = props + + const isPressable = !!WrapperProps.onPress + const isHeadingPresent = !!(HeadingComponent || heading || headingTx) + const isContentPresent = !!(ContentComponent || content || contentTx) + const isFooterPresent = !!(FooterComponent || footer || footerTx) + + const Wrapper: ComponentType = isPressable ? TouchableOpacity : View + const HeaderContentWrapper = verticalAlignment === "force-footer-bottom" ? View : Fragment + + const $containerStyle = [$containerPresets[preset], $containerStyleOverride] + const $headingStyle = [ + $headingPresets[preset], + (isFooterPresent || isContentPresent) && { marginBottom: spacing.micro }, + $headingStyleOverride, + HeadingTextProps?.style, + ] + const $contentStyle = [ + $contentPresets[preset], + isHeadingPresent && { marginTop: spacing.small }, + isFooterPresent && { marginBottom: spacing.small }, + $contentStyleOverride, + ContentTextProps?.style, + ] + const $footerStyle = [ + $footerPresets[preset], + (isHeadingPresent || isContentPresent) && { marginTop: spacing.micro }, + $footerStyleOverride, + FooterTextProps?.style, + ] + const $alignmentWrapperStyle = [ + $alignmentWrapper, + { justifyContent: $alignmentWrapperFlexOptions[verticalAlignment] }, + LeftComponent && { marginStart: spacing.medium }, + RightComponent && { marginEnd: spacing.medium }, + ] + + return ( + + {LeftComponent} + + + + {HeadingComponent || + (isHeadingPresent && ( + + ))} + + {ContentComponent || + (isContentPresent && ( + + ))} + + + {FooterComponent || + (isFooterPresent && ( + + ))} + + + {RightComponent} + + ) +} + +const $containerBase: ViewStyle = { + borderRadius: spacing.large, + padding: spacing.small, + // borderWidth: 1, + shadowColor: colors.palette.neutral600, + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 5, + minHeight: 86, + flexDirection: "row", +} + +const $alignmentWrapper: ViewStyle = { + flex: 1, + alignSelf: "stretch", +} + +const $alignmentWrapperFlexOptions = { + top: "flex-start", + center: "center", + "space-between": "space-between", + "force-footer-bottom": "space-between", +} as const + +const $containerPresets = { + default: [ + $containerBase, + { + backgroundColor: colors.palette.neutral100, + borderColor: colors.palette.neutral300, + }, + ] as StyleProp, + + reversed: [ + $containerBase, + { backgroundColor: colors.palette.neutral800, borderColor: colors.palette.neutral500 }, + ] as StyleProp, +} + +const $headingPresets: Record = { + default: {}, + reversed: { color: colors.palette.neutral100 }, +} + +const $contentPresets: Record = { + default: {}, + reversed: { color: colors.palette.neutral100 }, +} + +const $footerPresets: Record = { + default: {}, + reversed: { color: colors.palette.neutral100 }, +} diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx new file mode 100644 index 00000000..da9456c3 --- /dev/null +++ b/src/components/EmptyState.tsx @@ -0,0 +1,219 @@ +import React from "react" +import { Image, ImageProps, ImageStyle, StyleProp, TextStyle, View, ViewStyle } from "react-native" +import { translate } from "../i18n" +import { spacing } from "../theme" +import { Button, ButtonProps } from "./Button" +import { Text, TextProps } from "./Text" + +// const sadFace = require("../../assets/images/sad-face.png") + +interface EmptyStateProps { + /** + * An optional prop that specifies the text/image set to use for the empty state. + */ + preset?: keyof typeof EmptyStatePresets + /** + * Style override for the container. + */ + style?: StyleProp + /** + * An Image source to be displayed above the heading. + */ + imageSource?: ImageProps["source"] + /** + * Style overrides for image. + */ + imageStyle?: StyleProp + /** + * Pass any additional props directly to the Image component. + */ + ImageProps?: Omit + /** + * The heading text to display if not using `headingTx`. + */ + heading?: TextProps["text"] + /** + * Heading text which is looked up via i18n. + */ + headingTx?: TextProps["tx"] + /** + * Optional heading options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + headingTxOptions?: TextProps["txOptions"] + /** + * Style overrides for heading text. + */ + headingStyle?: StyleProp + /** + * Pass any additional props directly to the heading Text component. + */ + HeadingTextProps?: TextProps + /** + * The content text to display if not using `contentTx`. + */ + content?: TextProps["text"] + /** + * Content text which is looked up via i18n. + */ + contentTx?: TextProps["tx"] + /** + * Optional content options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + contentTxOptions?: TextProps["txOptions"] + /** + * Style overrides for content text. + */ + contentStyle?: StyleProp + /** + * Pass any additional props directly to the content Text component. + */ + ContentTextProps?: TextProps + /** + * The button text to display if not using `buttonTx`. + */ + button?: TextProps["text"] + /** + * Button text which is looked up via i18n. + */ + buttonTx?: TextProps["tx"] + /** + * Optional button options to pass to i18n. Useful for interpolation + * as well as explicitly setting locale or translation fallbacks. + */ + buttonTxOptions?: TextProps["txOptions"] + /** + * Style overrides for button. + */ + buttonStyle?: ButtonProps["style"] + /** + * Style overrides for button text. + */ + buttonTextStyle?: ButtonProps["textStyle"] + /** + * Called when the button is pressed. + */ + buttonOnPress?: ButtonProps["onPress"] + /** + * Pass any additional props directly to the Button component. + */ + ButtonProps?: ButtonProps +} + +const EmptyStatePresets = { + generic: { + imageSource: null, + heading: translate("emptyStateComponent.generic.heading"), + content: translate("emptyStateComponent.generic.content"), + button: translate("emptyStateComponent.generic.button"), + }, +} as const + +/** + * A component to use when there is no data to display. It can be utilized to direct the user what to do next. + * + * - [Documentation and Examples](https://github.com/infinitered/ignite/blob/master/docs/Components-EmptyState.md) + */ +export function EmptyState(props: EmptyStateProps) { + const preset = EmptyStatePresets[props.preset] ? EmptyStatePresets[props.preset] : undefined + + const { + button = preset?.button, + buttonTx, + buttonOnPress, + buttonTxOptions, + content = preset?.content, + contentTx, + contentTxOptions, + heading = preset?.heading, + headingTx, + headingTxOptions, + imageSource = preset?.imageSource, + style: $containerStyleOverride, + buttonStyle: $buttonStyleOverride, + buttonTextStyle: $buttonTextStyleOverride, + contentStyle: $contentStyleOverride, + headingStyle: $headingStyleOverride, + imageStyle: $imageStyleOverride, + ButtonProps, + ContentTextProps, + HeadingTextProps, + ImageProps, + } = props + + const isImagePresent = !!imageSource + const isHeadingPresent = !!(heading || headingTx) + const isContentPresent = !!(content || contentTx) + const isButtonPresent = !!(button || buttonTx) + + const $containerStyles = [$containerStyleOverride] + const $imageStyles = [ + $image, + (isHeadingPresent || isContentPresent || isButtonPresent) && { marginBottom: spacing.micro }, + $imageStyleOverride, + ImageProps?.style, + ] + const $headingStyles = [ + $heading, + isImagePresent && { marginTop: spacing.micro }, + (isContentPresent || isButtonPresent) && { marginBottom: spacing.micro }, + $headingStyleOverride, + HeadingTextProps?.style, + ] + const $contentStyles = [ + $content, + (isImagePresent || isHeadingPresent) && { marginTop: spacing.micro }, + isButtonPresent && { marginBottom: spacing.micro }, + $contentStyleOverride, + ContentTextProps?.style, + ] + const $buttonStyles = [ + (isImagePresent || isHeadingPresent || isContentPresent) && { marginTop: spacing.extraLarge }, + $buttonStyleOverride, + ButtonProps?.style, + ] + + return ( + + {isImagePresent && } + + {isHeadingPresent && ( + + )} + + {isContentPresent && ( + + )} + + {isButtonPresent && ( +