@giabaojs/react-native-tipkit
v0.1.1
Published
TipKit for React Native: native iOS in-app tips with popover and inline views, display rules, events and frequency control (iOS 17+)
Downloads
90
Maintainers
Readme
@giabaojs/react-native-tipkit
Apple TipKit for React Native — real native tips with anchored popovers and inline cards, driven by TipKit's own rules engine: event donations with count thresholds, display frequency, max display counts, persistent invalidation, and status updates streamed to JS.

Why this library?
The React Native ecosystem has plenty of JS-drawn "tooltip" libraries, but none of them expose what makes TipKit interesting: the native rules/eligibility engine. TipKit decides when a tip should appear — after an event happened N times, at most M times ever, no more than one tip per day — and persists all of that across launches in its own datastore. This library bridges that engine to React Native instead of reimplementing a lookalike in JS:
Tips.configurewith display frequency + datastore reset- Dictionary-driven tips (
title,message, SF Symbol image, action buttons) - Event donations & count-threshold rules evaluated by TipKit itself (see How it works)
Tips.MaxDisplayCount,invalidate(reason:),Tip.statusUpdates→ JS eventsTipUIPopoverViewControlleranchored to any React Native view (<TipAnchor>)TipUIViewas a self-sizing Fabric component (<InlineTip>)
iOS-only by design. TipKit is an Apple framework. On Android (and iOS < 17) every call is a clean no-op: promises resolve, getStatus resolves 'unavailable', <TipAnchor> renders its children unchanged, <InlineTip> renders nothing. You can ship the same code to both platforms without a single Platform.OS check.
Requirements
| | | | --- | --- | | React Native | 0.80+ (New Architecture only — TurboModule + Fabric) | | iOS | 17.0+ at runtime (the pod compiles fine with lower deployment targets) | | Android | any — compiles and no-ops |
Installation
npm install @giabaojs/react-native-tipkit
# or
yarn add @giabaojs/react-native-tipkit
cd ios && pod installQuick start
import {
configure,
registerTips,
donate,
invalidate,
addTipActionListener,
TipAnchor,
InlineTip,
} from '@giabaojs/react-native-tipkit';
// 1. Configure once at startup (before tips can display)
await configure({ displayFrequency: 'immediate' });
// 2. Register tips (plain data — no macros, no native code)
registerTips([
{
id: 'filter-tip',
title: 'Try a filter',
message: 'Give your photo a fresh look.',
sfSymbol: 'camera.filters',
maxDisplayCount: 3,
actions: [{ id: 'open-filters', title: 'Show me' }],
},
{
id: 'pro-tip',
title: 'Pro tools unlocked',
events: [{ id: 'opened-editor', threshold: 3 }], // TipKit event rule
},
]);
// 3. Anchor a popover tip to any view
<TipAnchor tipId="filter-tip">
<FilterButton />
</TipAnchor>;
// ...or render an inline tip card (self-sizing)
<InlineTip tipId="pro-tip" />;
// 4. Donate events — TipKit's rules engine decides when the tip shows
await donate('opened-editor');
// 5. React to tip action buttons
addTipActionListener(({ tipId, actionId }) => {
if (actionId === 'open-filters') {
openFilters();
invalidate(tipId, 'actionPerformed');
}
});API
Full reference with types: docs/API.md
| Export | Description |
| --- | --- |
| configure(options?) | Tips.configure — displayFrequency: 'immediate' \| 'hourly' \| 'daily' \| 'weekly' \| 'monthly', resetDatastore |
| registerTips(tips) | Register dictionary-driven tip definitions (validated in JS, materialized as native Tips) |
| donate(eventId) | Tips.Event(id:).donate() — feeds event-count rules |
| invalidate(tipId, reason?) | Tip.invalidate(reason:) — permanently dismisses a tip |
| getStatus(tipId) | 'pending' \| 'available' \| 'invalidated' \| 'unavailable' |
| isSupported() | true on iOS 17+, false elsewhere |
| addTipStatusListener(cb) | Streams native Tip.statusUpdates |
| addTipActionListener(cb) | Fires when a tip's action button is tapped |
| <TipAnchor tipId> | Presents TipUIPopoverViewController anchored to the wrapped view |
| <InlineTip tipId style?> | Renders TipUIView inline; height is measured natively and self-applied |
Platform matrix
| Platform | Behavior |
| --- | --- |
| iOS 17+ (New Architecture) | Full native TipKit |
| iOS < 17 | Graceful no-op (isSupported() === false, getStatus → 'unavailable') |
| Android | Graceful no-op — module compiles and resolves, components render children/nothing |
| Old Architecture | Not supported (TurboModule + Fabric only) |
Limitations (honest edition)
TipKit's ergonomic API is macro- and static-Swift-heavy. Here is exactly where the bridge stands — see docs/how-tipkit-works.md for the full analysis:
- Event-count rules are real TipKit rules. The
#Rulemacro refuses runtime values, but the publicTips.Ruleinitializer it expands to accepts them (PredicateExpressions.build_Arg(threshold)). Only the rule shape is fixed at compile time: this library ships thedonations.count >= thresholdtemplate. Time-windowed rules (donatedWithin) and parameter-based rules (@Parameter) are not exposed yet — if your eligibility logic needs them, compute it app-side and register/invalidate tips accordingly (that part is then your logic, not TipKit's). Tips.configureruns once per process. Callingconfigureagain in the same session is a no-op for options (e.g. you cannot changedisplayFrequencywithout an app restart).resetDatastoreordering. TipKit only honors a reset before the firstconfigureof a process. If you requestresetDatastore: truelater, the bridge attempts it, and when TipKit refuses, persists a flag and applies the reset at the start of the next launch.- Tips must be registered per launch. TipKit persists eligibility state (donations, display counts, invalidations) in its datastore, but the tip definitions live in JS — call
registerTipson every app start before rendering anchors. - Custom styling of the tip UI (
viewStyle, corner radius, colors) is not exposed yet; you get the system look. Tips.showAllTipsForTestingis metatype-based; with a single dynamic tip type it would be all-or-nothing, so it is not exposed. UseresetDatastorein development instead.
Troubleshooting
- Tip never shows — check the order:
configure()→registerTips()→ render<TipAnchor>/<InlineTip>. A tip that was closed or invalidated stays invalidated forever until you reset the datastore. - Popover appeared once and never again — that is TipKit's persistence working as intended (
maxDisplayCount, invalidation on close). Useconfigure({ resetDatastore: true })at startup while developing. - Second tip doesn't show with
displayFrequencyset — also intended: frequency is a global budget ("at most one new tip per hour/day/…"). Use'immediate'to disable. getStatusresolves'unavailable'— the platform is unsupported or the tip id was never registered in this session.- Build error
TipKit not found— make sure you build with Xcode 15+ / iOS 17 SDK. The framework is weak-linked, so apps still run on older iOS versions.
Example app
The example is Ledger, a small spend-tracker with a dark-first design system (example/src/theme.ts) and light-mode support. Each section gives one TipKit capability a natural home:
| Section | Demonstrates |
| --- | --- |
| Quick actions row | A popover tip anchored to the Split control (<TipAnchor>), with an action button and maxDisplayCount |
| Log activity | An event-driven tip on Insights, gated by donations.count >= 3, with a live n / 3 progress indicator so you can watch the native rule advance |
| Preferences list | A self-sizing inline tip (<InlineTip>) sitting between two real settings rows, with invalidate-on-action |
| Tip registry | Live getStatus + Tip.statusUpdates for every registered tip |
| Developer panel | configure frequency switching and resetDatastore, with honest notes about the once-per-process limitation |
| Native event stream | Everything the native layer emitted — status changes, action taps, donations |
All icons are drawn from plain <View>s and the tips use SF Symbols, so the example ships no image assets and has no third-party artwork to license.
yarn
yarn example iosContributing
See CONTRIBUTING.md for the development workflow.
License
MIT © Bao Nguyen
