diff --git a/apps/docs/docs/getting-started/installation/_mobileContent.mdx b/apps/docs/docs/getting-started/installation/_mobileContent.mdx index 74f6580e87..60ab85c711 100644 --- a/apps/docs/docs/getting-started/installation/_mobileContent.mdx +++ b/apps/docs/docs/getting-started/installation/_mobileContent.mdx @@ -5,6 +5,10 @@ import { MDXArticle } from '@site/src/components/page/MDXArticle'; ## Installation +:::tip Starting a new project? +Check out our [templates](/getting-started/templates) for pre-configured starter apps with CDS already set up. +::: + To install the CDS library for React Native applications, run the following command: ```bash diff --git a/apps/docs/docs/getting-started/installation/_webContent.mdx b/apps/docs/docs/getting-started/installation/_webContent.mdx index df0695e27f..b6acc2e1b8 100644 --- a/apps/docs/docs/getting-started/installation/_webContent.mdx +++ b/apps/docs/docs/getting-started/installation/_webContent.mdx @@ -5,6 +5,10 @@ import { MDXArticle } from '@site/src/components/page/MDXArticle'; ## Installation +:::tip Starting a new project? +Check out our [templates](/getting-started/templates) for pre-configured starter apps with CDS already set up. +::: + To install the CDS library for React web applications, run the following command: ```bash diff --git a/apps/docs/docs/getting-started/templates/_mobileContent.mdx b/apps/docs/docs/getting-started/templates/_mobileContent.mdx new file mode 100644 index 0000000000..5798d8de87 --- /dev/null +++ b/apps/docs/docs/getting-started/templates/_mobileContent.mdx @@ -0,0 +1,105 @@ +import { MDXSection } from '@site/src/components/page/MDXSection'; +import { MDXArticle } from '@site/src/components/page/MDXArticle'; +import { TemplateCard } from '@site/src/components/page/TemplateCard'; +import { HStack } from '@coinbase/cds-web/layout'; +import ThemedImage from '@theme/ThemedImage'; + + + + +## Get started + +The easiest way to get started with CDS on mobile is with a template. The Expo template includes the required CDS packages, dependencies, and pre-configured settings with a working example application to help you start building. + + + + } + /> + + + + + + + + +## Installation + +To create a new project from the template, use `gitpick` to bootstrap your application: + +### Expo + +```bash +npx -y gitpick coinbase/cds/tree/master/templates/expo-app cds-expo +cd cds-expo +``` + + + + + + + +## Setup + +After creating your project, install dependencies and start developing: + +```bash +# We suggest using nvm to manage Node.js versions +nvm install +nvm use + +# Enable corepack for package manager setup +corepack enable + +# Install dependencies +yarn + +# Start development server +yarn start +``` + + + + + + + +## What's included + +All templates come pre-configured with: + +- Latest CDS packages (`@coinbase/cds-mobile`, `@coinbase/cds-icons`, etc.) +- TypeScript configuration +- Example components demonstrating common UI patterns +- Theme setup with CDS default theme +- Navigation with React Navigation + + + + + + + +## Next steps + +After setting up your template, learn how to customize and extend CDS: + +- [Theming](/getting-started/theming) - Customize colors, spacing, and typography +- [Installation](/getting-started/installation) - Manual installation and setup options + + + diff --git a/apps/docs/docs/getting-started/templates/index.mdx b/apps/docs/docs/getting-started/templates/index.mdx index c126ec318e..821b28e699 100644 --- a/apps/docs/docs/getting-started/templates/index.mdx +++ b/apps/docs/docs/getting-started/templates/index.mdx @@ -1,7 +1,7 @@ --- id: templates title: Templates -platform_switcher_options: { web: true, mobile: false } +platform_switcher_options: { web: true, mobile: true } hide_title: true --- @@ -11,10 +11,17 @@ import { ContentHeader } from '@site/src/components/page/ContentHeader'; import { ContentPageContainer } from '@site/src/components/page/ContentPageContainer'; import WebContent, { toc as webContentToc } from './_webContent.mdx'; +import MobileContent, { toc as mobileContentToc } from './_mobileContent.mdx'; import webMetadata from './webMetadata.json'; +import mobileMetadata from './mobileMetadata.json'; - - } webContentToc={webContentToc} /> + + } + webContentToc={webContentToc} + mobileContent={} + mobileContentToc={mobileContentToc} + /> diff --git a/apps/docs/docs/getting-started/templates/mobileMetadata.json b/apps/docs/docs/getting-started/templates/mobileMetadata.json new file mode 100644 index 0000000000..da19f1de1c --- /dev/null +++ b/apps/docs/docs/getting-started/templates/mobileMetadata.json @@ -0,0 +1,3 @@ +{ + "description": "Get started quickly with a pre-built Expo template configured with CDS components and best practices." +} diff --git a/apps/docs/static/img/logos/frameworks/expo_dark.png b/apps/docs/static/img/logos/frameworks/expo_dark.png new file mode 100644 index 0000000000..52ae390f3b Binary files /dev/null and b/apps/docs/static/img/logos/frameworks/expo_dark.png differ diff --git a/apps/docs/static/img/logos/frameworks/expo_light.png b/apps/docs/static/img/logos/frameworks/expo_light.png new file mode 100644 index 0000000000..ef1c5458bc Binary files /dev/null and b/apps/docs/static/img/logos/frameworks/expo_light.png differ diff --git a/templates/expo-app/.gitignore b/templates/expo-app/.gitignore new file mode 100644 index 0000000000..319aa00b9e --- /dev/null +++ b/templates/expo-app/.gitignore @@ -0,0 +1,33 @@ +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo diff --git a/templates/expo-app/.nvmrc b/templates/expo-app/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/templates/expo-app/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/templates/expo-app/.yarnrc.yml b/templates/expo-app/.yarnrc.yml new file mode 100644 index 0000000000..c3ff0bb6bc --- /dev/null +++ b/templates/expo-app/.yarnrc.yml @@ -0,0 +1,4 @@ +nodeLinker: node-modules + + + diff --git a/templates/expo-app/App.tsx b/templates/expo-app/App.tsx new file mode 100644 index 0000000000..55e97a1df9 --- /dev/null +++ b/templates/expo-app/App.tsx @@ -0,0 +1,137 @@ +import React, { memo, useState, useCallback } from 'react'; +import { ScrollView } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useFonts } from 'expo-font'; +import { Inter_400Regular, Inter_600SemiBold } from '@expo-google-fonts/inter'; + +import type { ColorScheme } from '@coinbase/cds-common'; +import type { TabValue } from '@coinbase/cds-common/tabs/useTabs'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { defaultTheme } from '@coinbase/cds-mobile/themes/defaultTheme'; +import { VStack, HStack, Box } from '@coinbase/cds-mobile/layout'; +import { TabbedChips } from '@coinbase/cds-mobile/alpha/tabbed-chips/TabbedChips'; +import { PortalProvider } from '@coinbase/cds-mobile/overlays/PortalProvider'; +import { StatusBar, ThemeProvider } from '@coinbase/cds-mobile/system'; + +import { Navbar } from './components/Navbar'; +import { AssetList } from './components/AssetList'; +import { CardList } from './components/CardList'; +import { AssetCarousel } from './components/AssetCarousel'; +import { AssetChart } from './components/AssetChart'; +import { TabBarButton } from './components/TabBarButton'; + +const chipTabs = [ + { id: 'all', label: 'All' }, + { id: 'crypto', label: 'Crypto' }, + { id: 'nfts', label: 'NFTs' }, + { id: 'defi', label: 'DeFi' }, + { id: 'earn', label: 'Earn' }, +]; + +const CdsSafeAreaProvider = memo(({ children }: React.PropsWithChildren) => { + const theme = useTheme(); + return ( + {children} + ); +}); + +export default function App() { + const [activeColorScheme, setActiveColorScheme] = useState('light'); + const [fontsLoaded] = useFonts({ + CoinbaseIcons: require('@coinbase/cds-icons/fonts/native/CoinbaseIcons.ttf'), + Inter_400Regular, + Inter_600SemiBold, + }); + + const toggleColorScheme = useCallback( + () => setActiveColorScheme((s) => (s === 'light' ? 'dark' : 'light')), + [], + ); + + if (!fontsLoaded) { + return null; + } + + return ( + + + + + + + + + + + ); +} + +function AppContent({ toggleColorScheme }: { toggleColorScheme: () => void }) { + const [activeChip, setActiveChip] = useState(chipTabs[0]); + const [activeNavTab, setActiveNavTab] = useState('home'); + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + setActiveNavTab('home')} + /> + setActiveNavTab('trade')} + /> + setActiveNavTab('explore')} + /> + setActiveNavTab('account')} + /> + + + + ); +} diff --git a/templates/expo-app/README.md b/templates/expo-app/README.md new file mode 100644 index 0000000000..885febf8e5 --- /dev/null +++ b/templates/expo-app/README.md @@ -0,0 +1,49 @@ +# Coinbase Design System - Expo Template + +A React Native mobile application template integrated with the Coinbase Design System (CDS). + +## Installation + +Use `gitpick` to create a new project from this template: + +```sh +npx -y gitpick coinbase/cds/tree/master/templates/expo-app cds-expo +cd cds-expo +``` + +## Setup + +We suggest [nvm](https://github.com/nvm-sh/nvm/tree/master) to manage Node.js versions. If you have it installed, you can use these commands to set the correct Node.js version. Using corepack ensures you have your package manager setup. + +```sh +nvm install +nvm use +corepack enable +yarn +``` + +## Development + +- `yarn start` - Start the Expo development server +- `yarn ios` - Run on iOS simulator +- `yarn android` - Run on Android emulator +- `yarn web` - Run in web browser + +## Dev Builds + +Some CDS components require native modules that are not available in Expo Go. If you use any of the following components, you will need to create a [development build](https://docs.expo.dev/develop/development-builds/introduction/): + +- `DatePicker` +- `openWebBrowser` +- `AndroidNavigationBar` + +To create a development build: + +```sh +npx expo prebuild +npx expo run:ios # or npx expo run:android +``` + +## Documentation + +Visit [cds.coinbase.com](https://cds.coinbase.com) for the latest CDS documentation and component examples. diff --git a/templates/expo-app/app.json b/templates/expo-app/app.json new file mode 100644 index 0000000000..d5a0b64d76 --- /dev/null +++ b/templates/expo-app/app.json @@ -0,0 +1,15 @@ +{ + "expo": { + "name": "cds-expo-app", + "slug": "cds-expo-app", + "version": "1.0.0", + "orientation": "portrait", + "userInterfaceStyle": "light", + "ios": { + "supportsTablet": true + }, + "android": { + "backgroundColor": "#ffffff" + } + } +} diff --git a/templates/expo-app/babel.config.js b/templates/expo-app/babel.config.js new file mode 100644 index 0000000000..9d89e13119 --- /dev/null +++ b/templates/expo-app/babel.config.js @@ -0,0 +1,6 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + }; +}; diff --git a/templates/expo-app/components/AssetCarousel.tsx b/templates/expo-app/components/AssetCarousel.tsx new file mode 100644 index 0000000000..7bf31422f7 --- /dev/null +++ b/templates/expo-app/components/AssetCarousel.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { Carousel, CarouselItem } from '@coinbase/cds-mobile/carousel'; +import { MediaCard } from '@coinbase/cds-mobile/cards'; +import { RemoteImage } from '@coinbase/cds-mobile/media'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { assets } from '@coinbase/cds-common/internal/data/assets'; + +const assetList = Object.values(assets); + +export function AssetCarousel() { + const theme = useTheme(); + const horizontalPadding = theme.space[2]; + const horizontalGap = theme.space[2]; + + return ( + + {assetList.map((asset) => ( + + + } + title={asset.symbol} + subtitle={asset.name} + description={ + + Explore + + } + onPress={() => {}} + /> + + ))} + + ); +} diff --git a/templates/expo-app/components/AssetChart.tsx b/templates/expo-app/components/AssetChart.tsx new file mode 100644 index 0000000000..3a465f47fe --- /dev/null +++ b/templates/expo-app/components/AssetChart.tsx @@ -0,0 +1,116 @@ +import React, { memo, useState, useCallback, useMemo, forwardRef } from 'react'; + +import type { TabValue } from '@coinbase/cds-common/tabs/useTabs'; +import { useTabsContext } from '@coinbase/cds-common/tabs/TabsContext'; +import { useTheme } from '@coinbase/cds-mobile'; +import { VStack } from '@coinbase/cds-mobile/layout'; +import { RemoteImage } from '@coinbase/cds-mobile/media'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { SectionHeader } from '@coinbase/cds-mobile/section-header'; +import { SegmentedTab } from '@coinbase/cds-mobile/tabs/SegmentedTab'; +import { + ChartBridgeProvider, + LineChart, + PeriodSelector, + PeriodSelectorActiveIndicator, +} from '@coinbase/cds-mobile-visualization'; +import { assets } from '@coinbase/cds-common/internal/data/assets'; +import { sparklineInteractiveData } from '@coinbase/cds-common/internal/visualizations/SparklineInteractiveData'; + +const btcColor = assets.btc.color; + +const tabs = [ + { id: 'hour', label: '1H' }, + { id: 'day', label: '1D' }, + { id: 'week', label: '1W' }, + { id: 'month', label: '1M' }, + { id: 'year', label: '1Y' }, + { id: 'all', label: 'All' }, +]; + +const BTCActiveIndicator = memo((props: any) => { + return ; +}); + +const BTCTab = memo( + forwardRef(({ label, ...props }: any, ref: any) => { + const { activeTab } = useTabsContext(); + const isActive = activeTab?.id === props.id; + const theme = useTheme(); + + const wrappedLabel = + typeof label === 'string' ? ( + + {label} + + ) : ( + label + ); + + return ; + }), +); + +const priceFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', +}); + +function formatPrice(price: number) { + return priceFormatter.format(price); +} + +export const AssetChart = memo(function AssetChart() { + const [timePeriod, setTimePeriod] = useState(tabs[0]); + + const sparklineTimePeriodData = useMemo(() => { + return sparklineInteractiveData[timePeriod.id as keyof typeof sparklineInteractiveData]; + }, [timePeriod]); + + const sparklineValues = useMemo(() => { + return sparklineTimePeriodData.map((d) => d.value); + }, [sparklineTimePeriodData]); + + const currentPrice = + sparklineInteractiveData.hour[sparklineInteractiveData.hour.length - 1].value; + + const onPeriodChange = useCallback((period: TabValue | null) => { + setTimePeriod(period || tabs[0]); + }, []); + + return ( + + + {formatPrice(currentPrice)}} + end={ + + + + } + title={Bitcoin} + /> + + + + + ); +}); diff --git a/templates/expo-app/components/AssetList.tsx b/templates/expo-app/components/AssetList.tsx new file mode 100644 index 0000000000..f9f5a6616c --- /dev/null +++ b/templates/expo-app/components/AssetList.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { VStack } from '@coinbase/cds-mobile/layout'; +import { Text } from '@coinbase/cds-mobile/typography'; +import { ListCell } from '@coinbase/cds-mobile/cells'; +import { Avatar } from '@coinbase/cds-mobile/media'; +import { assets as cdsAssets } from '@coinbase/cds-common/internal/data/assets'; + +const assetData = [ + { + key: 'btc', + name: 'Bitcoin', + symbol: 'BTC', + price: '$67,432.18', + change: '+2.41%', + }, + { + key: 'eth', + name: 'Ethereum', + symbol: 'ETH', + price: '$3,521.90', + change: '+1.83%', + }, + { + key: 'ada', + name: 'Cardano', + symbol: 'ADA', + price: '$0.6231', + change: '-0.82%', + }, +] as const; + +type AssetKey = keyof typeof cdsAssets; + +export function AssetList() { + return ( + + + Your assets + + {assetData.map((asset) => { + const cdsAsset = cdsAssets[asset.key as AssetKey]; + return ( + + {asset.change} + + } + media={ + + } + accessory="arrow" + onPress={() => {}} + /> + ); + })} + + ); +} diff --git a/templates/expo-app/components/CardList.tsx b/templates/expo-app/components/CardList.tsx new file mode 100644 index 0000000000..01652763ff --- /dev/null +++ b/templates/expo-app/components/CardList.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Dimensions } from 'react-native'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { Carousel, CarouselItem } from '@coinbase/cds-mobile/carousel'; +import { MessagingCard } from '@coinbase/cds-mobile/cards'; +import { Pictogram } from '@coinbase/cds-mobile/illustrations'; +import { Button } from '@coinbase/cds-mobile/buttons'; + +export function CardList() { + const theme = useTheme(); + const windowWidth = Dimensions.get('window').width; + const horizontalPadding = theme.space[2]; + const horizontalGap = theme.space[2]; + const carouselWidth = windowWidth - horizontalPadding * 2; + const itemWidth = (carouselWidth - horizontalGap) / 1.1; + + return ( + + + + } + mediaPlacement="end" + action="Get started" + onActionButtonPress={() => {}} + onDismissButtonPress={() => {}} + /> + + + + } + mediaPlacement="end" + action={ + + } + onDismissButtonPress={() => {}} + /> + + + + + } + mediaPlacement="end" + action="Start learning" + onActionButtonPress={() => {}} + onDismissButtonPress={() => {}} + /> + + + ); +} diff --git a/templates/expo-app/components/Navbar.tsx b/templates/expo-app/components/Navbar.tsx new file mode 100644 index 0000000000..c66754bf6f --- /dev/null +++ b/templates/expo-app/components/Navbar.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { HStack, Box } from '@coinbase/cds-mobile/layout'; +import { Avatar } from '@coinbase/cds-mobile/media'; +import { TopNavBar, NavBarIconButton, NavigationTitle } from '@coinbase/cds-mobile/navigation'; + +export function Navbar({ toggleColorScheme }: { toggleColorScheme: () => void }) { + const theme = useTheme(); + const insets = useSafeAreaInsets(); + const isDark = theme.activeColorScheme === 'dark'; + + return ( + + } + end={ + + + + + } + > + Home + + + ); +} diff --git a/templates/expo-app/components/TabBarButton.tsx b/templates/expo-app/components/TabBarButton.tsx new file mode 100644 index 0000000000..71aeaec067 --- /dev/null +++ b/templates/expo-app/components/TabBarButton.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Pressable, type StyleProp, type ViewStyle } from 'react-native'; + +import { useTheme } from '@coinbase/cds-mobile'; +import { VStack } from '@coinbase/cds-mobile/layout'; +import { Icon } from '@coinbase/cds-mobile/icons'; +import { Text } from '@coinbase/cds-mobile/typography'; +import type { IconName } from '@coinbase/cds-common/types'; + +export type TabBarButtonProps = { + icon: IconName; + label: string; + active?: boolean; + onPress?: () => void; + style?: StyleProp; +}; + +export function TabBarButton({ icon, label, active = false, onPress, style }: TabBarButtonProps) { + const theme = useTheme(); + + return ( + + + + + {label} + + + + ); +} diff --git a/templates/expo-app/metro.config.js b/templates/expo-app/metro.config.js new file mode 100644 index 0000000000..a49e94d80c --- /dev/null +++ b/templates/expo-app/metro.config.js @@ -0,0 +1,23 @@ +const path = require('path'); +const { getDefaultConfig } = require('expo/metro-config'); + +const config = getDefaultConfig(__dirname); + +config.resolver.unstable_enablePackageExports = true; + +// Force @react-spring/native to use its CJS entry point. +// The ESM (.modern.mjs) entry uses a __require("react-native") polyfill +// that Metro cannot resolve when package exports are enabled. +const originalResolveRequest = config.resolver.resolveRequest; +config.resolver.resolveRequest = (context, moduleName, platform) => { + if (moduleName === '@react-spring/native') { + return { + type: 'sourceFile', + filePath: path.resolve(__dirname, 'node_modules/@react-spring/native/dist/cjs/index.js'), + }; + } + const resolve = originalResolveRequest ?? context.resolveRequest; + return resolve(context, moduleName, platform); +}; + +module.exports = config; diff --git a/templates/expo-app/package.json b/templates/expo-app/package.json new file mode 100644 index 0000000000..3a4d624146 --- /dev/null +++ b/templates/expo-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "cds-expo-app", + "main": "expo/AppEntry.js", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "@expo-google-fonts/inter": "^0.2.3", + "@coinbase/cds-common": "^8", + "@coinbase/cds-icons": "^5", + "@coinbase/cds-illustrations": "^4", + "@coinbase/cds-mobile": "^8", + "@coinbase/cds-mobile-visualization": "beta", + "@dotlottie/react-player": "1.6.1", + "@lottiefiles/dotlottie-react": "0.6.5", + "@lottiefiles/react-lottie-player": "3.5.3", + "@react-navigation/native": "6.1.17", + "@react-navigation/native-stack": "6.9.26", + "@react-navigation/stack": "6.4.1", + "@shopify/react-native-skia": "1.2.3", + "expo": "~51.0.28", + "expo-status-bar": "~1.12.1", + "lottie-react-native": "6.7.0", + "metro": "0.80.12", + "react": "18.2.0", + "react-native": "0.74.5", + "react-native-date-picker": "5.0.13", + "react-native-gesture-handler": "~2.16.1", + "react-native-inappbrowser-reborn": "3.7.0", + "react-native-linear-gradient": "2.8.3", + "react-native-navigation-bar-color": "2.0.2", + "react-native-reanimated": "~3.10.1", + "react-native-safe-area-context": "4.10.5", + "react-native-screens": "3.31.1", + "react-native-svg": "15.2.0" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@types/react": "~18.2.0", + "typescript": "~5.3.0" + }, + "private": true, + "packageManager": "yarn@4.9.2" +} diff --git a/templates/expo-app/tsconfig.json b/templates/expo-app/tsconfig.json new file mode 100644 index 0000000000..ed8ca65f9f --- /dev/null +++ b/templates/expo-app/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "expo/tsconfig.base.json", + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "moduleResolution": "bundler" + } +}