Skip to content

Feat/example app#349

Draft
evan-masseau wants to merge 8 commits intorel/2.4.0from
feat/example-app
Draft

Feat/example app#349
evan-masseau wants to merge 8 commits intorel/2.4.0from
feat/example-app

Conversation

@evan-masseau
Copy link
Copy Markdown
Contributor

Description

Due Diligence

  • I have tested this on a simulator/emulator or a physical device, on iOS and Android (if applicable).
  • I have added sufficient unit/integration tests of my changes.
  • I have adjusted or added new test cases to team test docs, if applicable.
  • I am confident these changes are implemented with feature parity across iOS and Android (if applicable).

Release/Versioning Considerations

  • Patch Contains internal changes or backwards-compatible bug fixes.
  • Minor Contains changes to the public API.
  • Major Contains breaking changes.
  • Contains readme or migration guide changes.
    • If so, please merge to a feature branch so documentation updates only go live upon official release.
  • This is planned work for an upcoming release.
    • If no, author or reviewer should account for this in a release plan, or describe why not below.

Changelog / Code Overview

Test Plan

Related Issues/Tickets

evan-masseau and others added 2 commits April 21, 2026 14:02
Centralized theme tokens (colors, spacing, typography, radii) plus a shared
StyleSheet and a small palette of interaction components — ActionButton,
ProfileTextField, SectionHeader, ToggleButtons, Collapsible — used by the
rest of the example app.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Introduce PermissionHelper (location + push permission flows wrapping
react-native-permissions and @react-native-firebase/messaging) plus four
domain hooks — useAnalytics, useForms, useLocation, usePush — each owning
the state and SDK calls for one Klaviyo feature area. Hooks provide handlers
the UI layer can wire to buttons without touching the SDK directly.

- Location permission flow requires separate taps for WhenInUse → Always
  on iOS (iOS won't prompt twice in one interaction)
- usePush re-fetches APNs token on onTokenRefresh so Firebase's FCM token
  doesn't stomp the APNs token on iOS
- Firebase availability is memoized at module scope

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Comment thread example/src/components/ToggleButtons.tsx
…rage

Replace the legacy button-wall demo with an interactive, sectioned example
that covers the full Klaviyo SDK public API surface and initializes the SDK
from JavaScript instead of native code.

- App.tsx: SectionList layout with Profile & Events / Forms / Geofencing /
  Push sections. App is a pure shell — it owns no hook state, so sibling
  sections don't re-render when one section's state changes.
- Each section component colocates the domain hook it consumes
  (useAnalytics, useForms, useLocation, usePush). This sidesteps the
  memoization trap where lifting hooks into App would re-run every hook on
  any state change and invalidate any React.memo on children. No memo
  wrappers or useCallback/useMemo gymnastics are needed under this layout.
- Profile + Events share a single useAnalytics instance by living in one
  merged AnalyticsSection (Option C from the review discussion): a typed
  email in the Profile fields is the same profile events attribute to, and
  we avoid Context/Provider boilerplate in a demo app. The section renders
  the profile fields and an inline "Events" sub-header with the event
  buttons below.
- Profile fields: External ID / Email / Phone inputs with individual set
  buttons, plus a collapsible "Additional Attributes" accordion for
  first/last name, title, organization and a "Location" accordion for
  city/country/zip/lat/long, aggregate Set Profile button, Reset Profile
- Events: test event + Viewed Product, both with value + uniqueId + custom
  properties
- Push: Firebase-backed permission request, Set Push Token (label reads
  "APNs Push Token" on iOS and "Firebase Push Token" on Android), Set Badge
  Count (iOS-only, with number input)
- Forms: explicit Register / Unregister
- Geofencing: explicit Register / Unregister, Get Current Geofences modal
- Deep link handling via Klaviyo.handleUniversalTrackingLink in a Linking
  useEffect with a proper cleanup
- Env loading migrated to react-native-dotenv (.env + .env.example) with a
  typed @env declaration — no more try/require gymnastics
- index.js registers a Firebase background message handler before
  AppRegistry as required by @react-native-firebase/messaging
- Bumps @react-native-firebase/app and /messaging to ^24.0.0 for RN 0.81
  bridgeless compatibility; adds react-native-dotenv dev dep
- Drops legacy AppViewInterface, KlaviyoReactWrapper, RandomGenerators

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
evan-masseau and others added 3 commits April 21, 2026 14:57
Wire up native-level Firebase push integration with @react-native-firebase
and Klaviyo, using the JS-first init pattern. Both platforms compile and
launch without Firebase configured — push features are just disabled in
that mode — and light up end-to-end when a Firebase config file is present.

iOS:
- AppDelegate.mm: guard [FIRApp configure] on GoogleService-Info.plist
  presence, wire UNUserNotificationCenter delegate, preserve deep-link +
  universal-link handlers, keep commented native-init reference block
- Podfile: $RNFirebaseAsStaticFramework = true, static frameworks linkage
- Info.plist: UIBackgroundModes = [fetch, location, remote-notification],
  location usage descriptions, scheme registration
- Entitlements: aps-environment = development, wired via
  CODE_SIGN_ENTITLEMENTS in the Xcode project
- Bundle id normalized to com.klaviyoreactnativesdkexample (matches
  Firebase app id and Android applicationId)
- GoogleService-Info.plist reference added to the Xcode project so the
  file is bundled when integrators drop it in

Android:
- Remove dead initializeKlaviyoFromNative / publicApiKey /
  useNativeFirebase gradle→BuildConfig plumbing (nothing reads them)
- Conditionally apply com.google.gms.google-services plugin on the
  presence of app/google-services.json — lets the project build cleanly
  without push configured
- MainApplication.kt: clean commented reference for native init; primary
  init path stays in JS
- Add google-services.json.template as a placeholder so the gradle plugin
  has something to resolve until integrators add their own

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Subscribe to Klaviyo.registerFormLifecycleHandler on mount in useForms so
every shown/dismissed/CTA-click event is console.logged. Retain the event
stream in a ring-buffer (capped at 100 entries, FIFO) in state and expose a
FormLifecycleEventsModal (mirroring the GeofencesModal pattern) that renders
a chronological log with per-event timestamps, formId/formName, and
buttonLabel/deepLinkUrl for CTA-click events. Detail fields rendered via
JSON.stringify so the modal doubles as a protocol inspector — documented
valid empty strings show as "" rather than collapsing into a placeholder.
Event keys are a monotonic id assigned at insertion so FlatList reconciler
reuse is stable across prepends. FormsSection gets an ActionButton showing
the running event count that opens the modal.

Demonstrates the 2.4.0 registerFormLifecycleHandler API; subscribing
unconditionally is safe — the SDK only emits events while forms are
registered.

Part of MAGE-464

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
#345)

# Description

Part 3 of 4 in the RN example-app overhaul chain for
[MAGE-464](https://linear.app/klaviyo/issue/MAGE-464). Wires up
native-level Firebase push integration on both iOS and Android using the
JS-first init pattern. Both platforms compile and launch cleanly without
Firebase configured (push features are disabled in that mode); full push
flow lights up end-to-end when a Firebase config file is present.

## Due Diligence

- [x] I have tested this on a simulator/emulator or a physical device,
on iOS and Android (if applicable).
- [ ] I have added sufficient unit/integration tests of my changes.
- [ ] I have adjusted or added new test cases to team test docs, if
applicable.
- [x] I am confident these changes are implemented with feature parity
across iOS and Android (if applicable).

## Release/Versioning Considerations

- [x] `Patch` Contains internal changes or backwards-compatible bug
fixes.
- [ ] `Minor` Contains changes to the public API.
- [ ] `Major` Contains **breaking** changes.
- [ ] Contains readme or migration guide changes.
- [ ] This is planned work for an upcoming release.

## Changelog / Code Overview

### iOS

| File | Change |
|------|--------|
| `AppDelegate.mm` | Call `[FIRApp configure]` on launch (unconditional
— stub plist is the zero-config default, documented in #346); preserve
UN delegate, deep-link, universal-link, and silent-push handlers; retain
`getLaunchOptionsWithURL` helper for RN #32350 cold-start workaround;
keep native-init reference block commented |
| `Podfile` | `$RNFirebaseAsStaticFramework = true` so RNFirebase links
cleanly under `use_frameworks!` |
| `Info.plist` | `UIBackgroundModes = [location, remote-notification]`;
location usage strings; URL scheme registration |
| `*.entitlements` | `aps-environment = development`, wired via
`CODE_SIGN_ENTITLEMENTS` |
| `project.pbxproj` | Bundle id normalized to
`com.klaviyoreactnativesdkexample` (matches Firebase app id and Android
`applicationId`); `GoogleService-Info.plist` file reference added so
it's bundled when integrators drop it in |
| `.github/workflows/ios-build.yml` | Stub `GoogleService-Info.plist`
step so CI builds succeed without a real Firebase project (format-valid
values so `FirebaseApp.configure()` succeeds at launch; benign
backend-registration warning at runtime) |

### Android

| File | Change |
|------|--------|
| `gradle.properties`, `local.properties.template`, `app/build.gradle` |
Remove dead `initializeKlaviyoFromNative` / `publicApiKey` /
`useNativeFirebase` gradle→BuildConfig plumbing (nothing reads it) |
| `app/build.gradle` | Conditionally apply
`com.google.gms.google-services` plugin on the presence of
`app/google-services.json` — clean build without push configured |
| `MainApplication.kt` | Commented reference block for native init;
primary path stays in JS |

## Test Plan

- [x] iOS: build with stub `GoogleService-Info.plist` (values documented
in #346) — app launches, push section shows "Firebase not configured" UI
- [x] iOS: build with real plist — `[FIRApp configure]` runs, permission
request works (see known sim caveat below)
- [x] Android: clean build without `google-services.json` — gms plugin
not applied, app launches, push section shows "Firebase not configured"
UI
- [x] Android: clean build with real `google-services.json` — gms plugin
applies, Firebase init succeeds, token populates

### Known iOS simulator caveat

Apple has a documented regression on iOS 26.0 / 26.2 simulators where
`didRegisterForRemoteNotificationsWithDeviceToken:` never fires
(FB19400926, FB19404213; matches
[rnfirebase/#8937](invertase/react-native-firebase#8937)).
APNs token fetching works correctly on physical devices and iOS 18.x
simulators. Setup is correct — Apple's sim is broken.

## Related Issues/Tickets

Part of [MAGE-464](https://linear.app/klaviyo/issue/MAGE-464)

**Chained PR series:**
1. theme + components (merged in #342)
2. JS layer: permission helpers, hooks, app shell (merged in #343)
3. **This PR** — native platform setup (iOS + Android Firebase push)
4. docs — #346

Also stacked alongside: #347 (CI Play Store publish workflow, branched
off this PR).

Follow-up: [MAGE-534](https://linear.app/klaviyo/issue/MAGE-534) —
convert `AppDelegate.mm` to pure Swift.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 19dcb50. Configure here.

)

# Description

Part 4 of 4 in the RN example-app overhaul chain for
[MAGE-464](https://linear.app/klaviyo/issue/MAGE-464). Adds a live event
log for `Klaviyo.registerFormLifecycleHandler` — the new API introduced
in RN SDK 2.4.0 — so integrators have a working reference implementation
and a built-in protocol inspector for their QA sessions.

**Stacked on #345** (`ecm/example-app/4-native`). This PR should be
rebased onto `feat/example-app` (or master, whichever is the eventual
landing target) once #345 merges; the diff shown here is only the delta
on top of that base.

### What changed

- **`useForms`** — subscribes to `registerFormLifecycleHandler` on mount
(no toggle needed; SDK only emits while forms are registered). Stores
events in a ring-buffer (FIFO, capped at 100) so a long QA session
doesn't pin an ever-growing array.
- **`FormLifecycleEventsModal`** (new) — mirrors the `GeofencesModal`
structure. Renders a `FlatList` with per-event timestamps,
`formId`/`formName`, event type badge, and `buttonLabel`/`deepLinkUrl`
for CTA-click events. Keys are a monotonic id assigned at insertion for
stable FlatList reconciler reuse across prepends. Detail fields use
`JSON.stringify` so the modal functions as a protocol inspector —
`buttonLabel: ""` renders as `""` rather than collapsing (the SDK
documents empty-string as a valid value).
- **`FormsSection`** — new `ActionButton` showing a live count of
captured events that opens the modal.

## Due Diligence

- [ ] I have tested this on a simulator/emulator or a physical device,
on iOS and Android (if applicable).
- [ ] I have added sufficient unit/integration tests of my changes.
- [ ] I have adjusted or added new test cases to team test docs, if
applicable.
- [x] I am confident these changes are implemented with feature parity
across iOS and Android (if applicable). _(JS-only change; no platform
split.)_

## Release/Versioning Considerations

- [x] `Patch` Contains internal changes or backwards-compatible bug
fixes.
- [ ] `Minor` Contains changes to the public API.
- [ ] `Major` Contains **breaking** changes.
- [ ] Contains readme or migration guide changes.
- [ ] This is planned work for an upcoming release.

> Example app only — no public API changes, no version bump needed.

## Changelog / Code Overview

| File | Change |
|------|--------|
| `example/src/hooks/useForms.ts` | Subscribe to lifecycle handler on
mount; maintain ring-buffer event state; return event array + clear fn |
| `example/src/components/FormLifecycleEventsModal.tsx` | New modal
component — chronological event log with type badge, timestamps, and
detail inspector |
| `example/src/sections/FormsSection.tsx` | New ActionButton wiring
event count → modal |

## Test Plan

Here's how it looks
<img width="320" height="213" alt="Screenshot 2026-04-21 at 5 06 57 PM"
src="https://github.com/user-attachments/assets/102d6396-4fe3-4b68-9499-3432bd6aa926"
/>
<img width="354" height="765" alt="Screenshot 2026-04-21 at 5 06 46 PM"
src="https://github.com/user-attachments/assets/c043f6df-b027-48e5-91c9-8c4b9bb6f2f3"
/>


- [ ] iOS: trigger a form show event — confirm it appears in the modal
with correct `formId`, `formName`, and timestamp
- [ ] iOS: trigger a form dismiss — confirm `dismissed` event type badge
appears
- [ ] iOS: trigger a form CTA click — confirm `buttonLabel` and
`deepLinkUrl` are rendered (not collapsed), including when `buttonLabel`
is an empty string
- [ ] iOS: generate >100 events — confirm older entries are evicted
(ring-buffer, not unbounded growth)
- [ ] Android: repeat the above — same behavior expected (JS-only
change)
- [ ] Confirm Clear button resets the list and the count badge on
`FormsSection` goes to 0
- [ ] Confirm app cold-starts cleanly with no forms registered — no
crash, event count shows 0

## Related Issues/Tickets

Part of [MAGE-464](https://linear.app/klaviyo/issue/MAGE-464)

**Chained PR series:**
1. theme + components (merged in #342)
2. JS layer: permission helpers, hooks, app shell (merged in #343)
3. native platform setup — #345
4. **This PR** — lifecycle event subscription + debug modal
5. docs — #346

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant