Skip to content

Conversation

codinsonn
Copy link
Contributor

@codinsonn codinsonn commented Mar 16, 2023

What

  • Set-up @bothrs/expo package
  • Set-up example project
  • Add parseConstants() util
  • Add parseGradient() util
  • Add conditionalMarkup() util
  • Add <Gradient /> component
  • Add useSvgDimensions() hook
  • Add <SentryProvider/> component
  • Add <DynamicBottomSheet/> component
  • Add README.md

Ticket

[Notion] Identify recurring patterns and ideate solutions for reusability

Demo

yarn workspace @bothrs/expo example start

README

Show README.md content

@bothrs/expo

A set of reusable Expo components, hooks, and utilities for Bothrs projects

Installation

npm install @bothrs/expo

Providers

SentryProvider

A provider that wraps your app with Sentry init and an ErrorBoundary component. This is useful for catching errors and reporting them to Sentry.

App.tsx

import { SentryProvider } from '@bothrs/expo'

// ...

<SentryProvider
    sentryConfig={{
        dsn: '', // YOUR DSN FROM SENTRY HERE
        enabled: isLocalDevelopment,
        enableInExpoDevelopment: true, // Turn this off when you're done testing
        debug: true,
        release: `${appName}@${appVersion}`,
        environment: 'frontend',
        tracesSampleRate: 1,
    }}
    renderErrorScreen={() => <CustomErrorScreen />}
>
  <YourApp />
</SentryProvider>

Components

DynamicBottomSheet

A bottom sheet that automatically expands and collapses based on the content inside of it.

App.tsx

import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'

// ...

<BottomSheetModalProvider>
  <YourApp />
</BottomSheetModalProvider>

SomeScreen.tsx

import { BottomSheetModal } from '@gorhom/bottom-sheet'
import { DynamicBottomSheet } from '@bothrs/expo'

// ...

const bottomSheetRef = useRef<BottomSheetModal>(null)

// -- Handlers --

const onShowBottomSheet = () => {
    bottomSheetRef.current?.present()
}

// ...

<>
    <DynamicBottomSheet
        ref={bottomSheetRef}
        // snapPoints={['50%', '100%']}
    >
        <YourContent />
    </DynamicBottomSheet>
</>

Gradient

A component that renders a gradient background from a CSS linear gradient string.

import { Gradient } from '@bothrs/expo'

// ...

<Gradient
    linearGradient="linear-gradient(180.0deg, rgba(15, 15, 15, 0) 0%, rgba(15, 15, 15, 0.96) 85%)"
/>

Utils

parseGradient

A utility function that parses a CSS linear gradient string and returns an array of colors and locations to use with expo-linear-gradient.

import { parseGradient } from '@bothrs/expo'
import { LinearGradient } from 'expo-linear-gradient'

const { colors, locations } = parseGradient(
    'linear-gradient(180.0deg, rgba(15, 15, 15, 0) 0%, rgba(15, 15, 15, 0.96) 85%)'
)

<LinearGradient
    colors={colors} // ['rgba(15, 15, 15, 0)', 'rgba(15, 15, 15, 0.96)']
    locations={locations} // [0, 0.85]
/>

parseConstants

A utility function that parses the Expo Constants object and returns a new object with only the properties you need, regardles of whether you're in Expo Go, a Dev Client or the Standalone Production App.

Useful to e.g. get the app version and name. Determine which back-end to contact per environment, including your local back-end url in development mode, so you can test on your own device without extra tunnels.

constants.tsx

import Constants from 'expo-constants'
import { parseConstants } from '@bothrs/expo'

// To determine yourself based on your branch strategy
const branchConfig = {
    devBranches: ['dev-preview'], // -> branchEnv = 'development'
    stageBranches: ['staging'], // -> branchEnv = 'staging'
    prodBranches: ['prod'], // -> branchEnv = 'production'
}

// Parse constants using your branchConfig
const {
    localUrl,
    appName,
    appVersion,
    sdkVersion,
    iosBuildnumber,
    androidVersionCode,
    branchName,
    branchEnv,
    isLocalDevelopment,
    isDevelopment,
    isStaging,
    isProduction,
} = parseConstants(Constants, branchConfig)

// Determine your back-end url based on the environment info

let api = localUrl // <-- Your local back-end url based on debugger IP
if (!isLocalDevelopment) api = 'https://dev-api.example.com' 
if (isStaging) api = 'https://staging-api.example.com'
if (isProduction) api = 'https://api.example.com'

// Re-export what you need
export {
    // based on constants
    api
    ...
    // parsed constants
    localUrl,
    appName,
    appVersion,
    sdkVersion,
    iosBuildnumber,
    androidVersionCode,
    branchName,
    branchEnv,
    isLocalDevelopment,
    isDevelopment,
    isStaging,
    isProduction,
}

conditionalMarkup

Helper to avoid ternaries in conditional styled-components styling.

import styled from 'styled-components/native'
import { conditionalMarkup } from '@bothrs/expo'

// ...

const StMarkupTest = styled.Text<{ orientation: 'landscape' | 'portrait' }>`
  ${({ orientation }) => conditionalMarkup(
    orientation === 'portrait' && 'text-decoration: underline;',
    orientation === 'portrait' && 'color: blue;',
    orientation === 'landscape' && 'color: red;',
    orientation === 'landscape' && 'font-weight: bold;',
  )}
`

Hooks

useSvgDimensions

SomeSvgIllustration.tsx

import { Dimensions } from 'react-native'
import { Svg, SvgProps, Path, ... } from 'react-native-svg'
import { useSvgDimensions } from '@bothrs/expo'

const SomeSvgIllustration = (props: SvgProps) => {
    // Map original svg dimensions to props for the svg component
    const { width, height, viewBox } = useSvgDimensions({
        originalWidth: 100,
        originalHeight: 100,
        containerWidth: Dimensions.get('window').width,
    })

    // -- Render --

    return (
        <Svg
            width={width}
            height={height}
            viewBox={viewBox}
            {...props}
        >
            {/* ... your SVG code here ... */}
        </Svg>
    )
}

@codinsonn codinsonn self-assigned this Mar 16, 2023
@codinsonn codinsonn marked this pull request as ready for review March 16, 2023 17:06
@codinsonn codinsonn requested review from TijsM, Emieldv, FabianMeul, a user, bramdeba, bramvanhoutte, BrentVanSteertegem, JaccoGoris, StefVerlinde, thgh and Vanatis and removed request for a user March 16, 2023 17:07
const localUrl: string | undefined = debuggerHostUrl?.split(':').shift() // prettier-ignore

// Names
const branchName: string | undefined = manifest?.releaseChannel || manifest2?.metadata?.branchName // prettier-ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

Q:

Is this now a thing we do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using branches?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If that's what you mean, I suppose the answer is yes, as classic updates are now no more:

https://docs.expo.dev/archive/classic-updates/introduction/

Classic Updates was a service by Expo that delivered updates to end-users using Expo CLI's expo publish command. It was supported by Expo through August 2022, when it was replaced by EAS Update.

https://docs.expo.dev/versions/latest/sdk/updates/#updatesreleasechannel

I do read that releaseChannel will always be 'default' from now on though, so I'll switch the order of these I think (or remove manifest?.releaseChannel entirely)

return BRANCH_ENV.production
}

const branchEnv = getCurrentEnvironment()
Copy link
Member

Choose a reason for hiding this comment

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

Why is this wrapped in a function?

const appName: string | undefined = expoConfig?.name || manifest?.name || manifest2?.name // prettier-ignore

// Versions
const appVersion: string = expoConfig?.version || manifest?.version || manifest2?.version // prettier-ignore
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistent naming: appVersion<->version

const { manifest, manifest2, expoConfig, platform } = Constants

// Local Debugger IP
const debuggerHostUrl: string | undefined = manifest?.debuggerHost || manifest2?.extra?.expoGo?.debuggerHost // prettier-ignore
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistent naming: debuggerHostUrl<-> debuggerHost

// Versions
const appVersion: string = expoConfig?.version || manifest?.version || manifest2?.version // prettier-ignore
const sdkVersion: string = expoConfig?.sdkVersion || manifest?.sdkVersion || manifest2?.sdkVersion // prettier-ignore
const iosBuildnumber: number | null | undefined = platform?.ios?.buildNumber
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistent casing of "n": Buildnumber<->buildNumber


/* --- <ErrorBoundary/> ------------------------------------------------------------------------ */

class ErrorBoundary extends React.Component<
Copy link
Member

Choose a reason for hiding this comment

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

Consider splitting up multiple components in 1 file. This ErrorBoundary is worth to be a component apart from Sentry.


export type ErrorBoundaryProps = {
children: React.ReactNode
renderErrorScreen: () => JSX.Element
Copy link
Member

Choose a reason for hiding this comment

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

Feature request: make this optional and provide a simple error screen with reload button.


// -- Effects --

useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

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

I would recommend to init Sentry in the global scope instead of inside useEffect. In which usecase do you pass dynamic props that would affect Sentry? It looks like there are 3 ways to disable sentry: enabled, dsn, silenceEnabledWarnings. Why is simply setting dsn not enough?

Comment on lines +19 to +23
export type BranchConfig = {
devBranches: string[]
stageBranches: string[]
prodBranches: string[]
}
Copy link
Member

Choose a reason for hiding this comment

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

Inconsistent dev<->development, stage<->staging, prod<->production

Consider retyping as below to improve consistency:

Suggested change
export type BranchConfig = {
devBranches: string[]
stageBranches: string[]
prodBranches: string[]
}
export type BranchConfig = Record<BranchEnvironment, string[]>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants