Skip to main content

UI Components

The SDK ships three React Native components for the full OTA lifecycle. They're opinionated, accessible, and themable. Drop them in and you're done — or use them as a starting point for a fully custom UI.

All three are auto-rendered by <SwiftPatchProvider> when their corresponding prop is enabled. You can also import and render them yourself for fine-grained control.

<UpdateBanner />

A subtle, non-modal banner that slides in when an update is ready to apply.

import { UpdateBanner } from '@swiftpatch/react-native';

<UpdateBanner
variant="subtle"
position="bottom"
behavior="dismissible"
/>

Rendered state:

┌───────────────────────────────────────────────┐
│ New update available │
│ v1.2.3 · Perf fixes + new feature [Later] │
│ [Apply now] │
└───────────────────────────────────────────────┘

Props

interface UpdateBannerProps {
variant?: 'subtle' | 'bold' | 'pulse';
position?: 'top' | 'bottom';
behavior?: 'dismissible' | 'persistent';
theme?: ThemeProp;
title?: string;
applyLabel?: string;
dismissLabel?: string;
onApply?: () => void;
onDismiss?: () => void;
style?: ViewStyle;
disabled?: boolean;
}
PropDefaultDescription
variant'subtle'subtle uses the theme surface color. bold uses the accent. pulse adds a gentle scale loop on the CTA.
position'bottom''top' or 'bottom'.
behavior'dismissible'Show or hide the "Later" button. Set 'persistent' to force an apply.
theme'auto''auto', 'light', 'dark', or a partial Theme object.
title, applyLabel, dismissLabelCopy overrides.
onApply, onDismissFire after the respective verbs complete.
styleOverride the container style.
disabledfalseNever render.

Accessibility

The banner is announced via accessibilityLiveRegion="polite" and respects prefers-reduced-motion — it skips the slide/fade animation when the OS flag is on.

<UpdateBlocker />

A full-screen modal that appears when a mandatory release is pending. The user cannot dismiss it — the only action is Apply.

import { UpdateBlocker } from '@swiftpatch/react-native';

<UpdateBlocker
logo={require('./assets/logo.png')}
accentColor="#ff3366"
title="Critical update"
body="A security fix is installing. This takes a few seconds."
applyLabel="Install now"
/>

Rendered state:

┌───────────────────────────────────────────────┐
│ │
│ [LOGO] │
│ │
│ Critical update │
│ │
│ A security fix is installing. │
│ This takes a few seconds. │
│ │
│ Downloading… 42% │
│ ████████░░░░░░░░░░░░░░ │
│ │
│ [ Install now ] │
│ │
└───────────────────────────────────────────────┘

Props

interface UpdateBlockerProps {
logo?: ImageSourcePropType;
accentColor?: string;
title?: string;
body?: string;
applyLabel?: string;
theme?: ThemeProp;
style?: ViewStyle;
disabled?: boolean;
}

While the download is in progress the blocker shows a progress bar. Once the bundle is ready, the Apply CTA becomes active. On press, the SDK installs and restarts.

<UpdateOnboarding />

A three-step carousel shown exactly once, after the very first OTA is applied. Use it to educate new users about how updates work.

import { UpdateOnboarding } from '@swiftpatch/react-native';

<UpdateOnboarding
steps={[
{
title: 'Your app is now faster',
description: 'We updated in the background — no download, no wait.',
},
{
title: 'What is new',
description: 'Dark mode, better performance, and 3 bug fixes.',
},
{
title: 'You are all set',
description: 'Future updates will apply the same way.',
},
]}
/>

Props

interface UpdateOnboardingProps {
steps?: UpdateOnboardingStep[];
onDone?: () => void;
theme?: ThemeProp;
disabled?: boolean;
continueLabel?: string;
skipLabel?: string;
style?: ViewStyle;
}

interface UpdateOnboardingStep {
title: string;
description: string;
}

The "already shown" flag is persisted in native preferences. It never reappears on the same install, even after cold starts.

Theming

All three components share a single Theme type:

interface Theme {
background: string;
surface: string;
text: string;
textMuted: string;
accent: string;
accentText: string;
danger: string;
border: string;
}

type ThemeProp = 'auto' | 'light' | 'dark' | Partial<Theme>;

Default themes:

export const LIGHT_THEME: Theme = {
background: '#ffffff',
surface: '#f3f4f6',
text: '#0f172a',
textMuted: '#64748b',
accent: '#2563eb',
accentText: '#ffffff',
danger: '#dc2626',
border: '#e2e8f0',
};

export const DARK_THEME: Theme = {
background: '#0f0f23',
surface: '#1a1a3e',
text: '#ffffff',
textMuted: '#94a3b8',
accent: '#3b82f6',
accentText: '#ffffff',
danger: '#ef4444',
border: '#252550',
};

Override a few colors

<UpdateBanner theme={{ accent: '#ff3366', accentText: '#ffffff' }} />

Override the full theme

import { LIGHT_THEME } from '@swiftpatch/react-native';

const MY_THEME = {
...LIGHT_THEME,
accent: '#ff3366',
background: '#fafafa',
};

<SwiftPatchProvider config={{ debug: __DEV__ }} theme={MY_THEME}>
<App />
</SwiftPatchProvider>

Force light / dark

<UpdateBanner theme="dark" />

Replacing the built-in UI

Turn it all off and render your own:

<SwiftPatchProvider
config={{ debug: __DEV__ }}
autoShowBanner={false}
blockOnMandatory={false}
>
<App />
<MyCustomUpdateBanner />
</SwiftPatchProvider>

Your custom component uses useSwiftpatchUpdate():

import { useSwiftpatchUpdate } from '@swiftpatch/react-native';

function MyCustomUpdateBanner() {
const { phase, pendingUpdate, applyNow, dismiss } = useSwiftpatchUpdate();

if (phase !== 'ready' || !pendingUpdate) return null;

return (
<MyBrandedToast
title={`v${pendingUpdate.version} is ready`}
primaryAction={{ label: 'Apply', onPress: applyNow }}
secondaryAction={{ label: 'Later', onPress: dismiss }}
/>
);
}

See Hooks for the full hook API.

Next steps

  • HooksuseSwiftpatchUpdate, useSwiftpatchProgress, useSwiftpatchReady.
  • Provider referenceautoShowBanner, blockOnMandatory, theme.
  • Methods — the imperative verbs the UI components call under the hood.