name: react-native-ios-bridge-implementation
description: >
Implements the iOS bridge layer for a React-Native MoEngage SDK feature.
This is Step 2b of the React-Native feature pipeline (parallel to or after
react-native-android-bridge-implementation). Produces the Bridge .h/.mm, Handler .h/.m,
Constants .h/.m, optional Util .h/.m, and podspec for a new sdk/
- name: "ticket_id" description: "JIRA ticket ID, e.g. 'MOEN-44072'. Extracted from command text if not supplied." optional: true
- name: "feature_description" description: "Natural language description of the feature. E.g. 'JWT authentication parity'."
- name: "contract_branch" description: "Branch in 'mobile-sdk-contracts' with the feature contract."
- name: "ios_plugin_version"
description: "Target MoEngagePlugin
pod version. E.g. '3.10.0'." - name: "ios_plugin_base_pr_url" description: "URL of the iOS-PluginBase PR from plugin-base-feature-implementation."
Overview
Implements the iOS Objective-C bridge inside the React-Native SDK repo (React-Native) for a
MoEngage feature whose plugin-base module already exists.
Prerequisite chain:
plugin-base-feature-implementation— creates/extends the plugin-base module ✅react-native-ios-bridge-implementation← you are here (parallel to Android bridge)react-native-ts-implementation— TypeScript models, spec, Handler, public API
Architecture standard: Follow the Cards module pattern exactly (sdk/cards/ios/).
Example files: Templates are in examples/ adjacent to this SKILL.md. Read each
template before generating the corresponding file.
Example Files Index
examples/
Bridge.h ← MoEngage<featureNameCamel>Bridge.h
Bridge.mm ← MoEngage<featureNameCamel>Bridge.mm
Constants.h ← MoEngage<featureNameCamel>ReactConstants.h
Constants.m ← MoEngage<featureNameCamel>ReactConstants.m
Handler.h ← MoEReactNative<featureNameCamel>Handler.h
Handler.m ← MoEReactNative<featureNameCamel>Handler.m
Util.h ← MoEngage<featureNameCamel>ReactUtil.h (only if promise or event methods)
Util.m ← MoEngage<featureNameCamel>ReactUtil.m (only if promise or event methods)
Phase 0 — Clarify Inputs
0.1 Extract ticket ID
Scan the user's full command for MOEN-\d+ → ticketId.
If not found in the command or parameters, ask before proceeding.
Phase 1 — Parse Inputs & Derive All Identifiers
1.1 Extract from contract_branch
Strip everything up to and including the first / or _MOEN-XXXXX_ prefix:
feature/experience_contracts→contractSuffix=experience_contractsMOEN-44072_jwt_contract→contractSuffix=jwt_contract
1.2 Identifiers table
| Identifier | Example | Rule |
|---|---|---|
ticketId |
MOEN-44072 |
MOEN-\d+ from raw command or parameter |
contractSuffix |
jwt_contract |
branch name after first / or _MOEN-XXXXX_ |
featureName |
jwt |
lowercase slug from feature_description |
featureNameCamel |
Jwt |
PascalCase of featureName |
contractDir |
authentication |
subdirectory found in contracts json/ after checkout |
rnSdkDir |
sdk/core |
see rule below |
iosBridgeName |
MoEReactBridge |
see rule below |
iosHandlerName |
MoEReactNativeHandler |
see rule below |
iosPluginBridge |
see rule below | see rule below |
iosModule |
see rule below | see rule below |
iosUtilName |
MoEngageReactUtils |
see rule below |
branchName |
feature/MOEN-44072-jwt_contract |
feature/<ticketId>-<contractSuffix> |
1.3 Resolve iosBridgeName, iosHandlerName, and iosUtilName
Look up the existing class names by rnSdkDir. Do not generate these from a formula — use the actual names from the codebase. The Util is a shared module-level class, never a feature-specific one.
rnSdkDir |
iosBridgeName |
iosHandlerName |
iosUtilName |
iosDelegateName |
iosDelegatePattern |
|---|---|---|---|---|---|
sdk/core |
MoEReactBridge |
MoEReactNativeHandler |
MoEngageReactUtils |
MoEngagePluginBridgeDelegate |
generic string: sendMessageWithEvent:message: |
sdk/cards |
MoEngageCardsBridge |
MoEReactNativeCardsHandler |
MoEngageCardsReactUtil |
MoEngageCardSyncDelegate |
typed enum: syncCompleteForEventType:withData: |
sdk/geofence |
MoEReactGeofence |
MoEReactNativeGeofenceHandler |
ask user | ask user | ask user |
sdk/inbox |
MoEReactInbox |
MoEReactNativeInboxHandler |
ask user | ask user | ask user |
sdk/personalize |
MoEngagePersonalizeBridge |
MoEReactNativePersonalizeHandler |
ask user | ask user | ask user |
| unknown | ask user | ask user | ask user | ask user | ask user |
1.5 Resolve rnSdkDir
Scan feature_description for a framework keyword and map to the existing SDK module directory:
Keyword in feature_description |
rnSdkDir |
|---|---|
core, analytics, inapps, or messaging |
sdk/core |
cards |
sdk/cards |
geofence |
sdk/geofence |
inbox |
sdk/inbox |
personalize |
sdk/personalize |
| none of the above | ask the user which SDK module to add the feature to |
Examples:
"JWT authentication parity from core"→sdk/core"get clicked cards count"→sdk/cards"start geofence monitoring"→sdk/geofence
1.6 Resolve iosPluginBridge and iosModule
Scan feature_description for a framework keyword:
| Keyword in feature_description | iosPluginBridge |
iosModule |
|---|---|---|
analytics, inapps, messaging, or core |
MoEngagePluginBridge |
MoEngagePluginBase |
| anything else (cards, geofence, inbox, …) | MoEngagePlugin<featureNameCamel>Bridge |
MoEngagePlugin<featureNameCamel> |
Examples:
"jwt support from core"→MoEngagePluginBridge/MoEngagePluginBase"set device attribute from analytics"→MoEngagePluginBridge/MoEngagePluginBase"get clicked cards count"→MoEngagePluginCardsBridge/MoEngagePluginCards"start geofence monitoring"→MoEngagePluginGeofenceBridge/MoEngagePluginGeofence
Phase 2 — Read Contracts
cd ../mobile-sdk-contracts
git fetch
git stash
git checkout <contract_branch>
- List
json/hybridToNative/to identifycontractDir- If no matching directory → list available dirs and ask the user
- For each
.jsoninjson/hybridToNative/<contractDir>/:- Filename (without
.json) = method name (camelCase) - File content = input payload shape
- Filename (without
- For each
.jsoninjson/nativeToHybrid/<contractDir>/:- File content = event/response payload shape
- Read all
.protofiles inprotos/<contractDir>/for field names and types
Method classification
| Condition | Type | iOS pattern | Files needed |
|---|---|---|---|
hybridToNative only |
fire-and-forget | RCT_EXPORT_METHOD(name:(NSString*)payload) |
Bridge + Handler only |
both hybridToNative + nativeToHybrid |
check plugin-base — see rule below | depends | depends |
nativeToHybrid only |
event | delegate method → sendEventWithName:body: |
Bridge + Handler + Util + Constants |
When both hybridToNative and nativeToHybrid exist, do NOT ask the user — instead read the plugin-base header for <iosPluginBridge> and check how the method returns data:
# Find the method declaration in the plugin-base header:
grep -n "<methodName>" ../ios-PluginBase/<module>/<iosPluginBridge>.h
| Plugin-base method signature | Type | iOS pattern | Files needed |
|---|---|---|---|
Has completionHandler:^(NSDictionary *result) block param |
promise | RCT_EXPORT_METHOD(name:payload resolve:resolve reject:reject) |
Bridge + Handler + Util |
| No completion block — data returned via delegate | event | RCT_EXPORT_METHOD(name:payload) + delegate → sendEventWithName:body: |
Bridge + Handler + Util + Constants |
Examples:
- (void)getSelfHandledInApps:(NSDictionary*)payload completionHandler:(void(^)(NSDictionary*))handler→ promise- (void)getSelfHandledInApps:(NSDictionary*)payload(no block, data via<iosDelegateName>) → event
If the plugin-base header cannot be found → ask the user.
Build a complete method table before writing any code.
Phase 3 — iOS Bridge Implementation
3.1 Check out the branch
cd React-Native
git fetch
git checkout feature/<ticketId>-<contractSuffix> # branch created by Android bridge step
If the branch does not exist yet (iOS-first flow):
git checkout -b feature/<ticketId>-<contractSuffix>
3.2 Check if iOS files already exist
ls <rnSdkDir>/ios/ 2>/dev/null
- Not found → scaffold full iOS layer (Steps 3.3–3.9)
- Found → read existing files first, then add only the missing methods
3.3 Constants (.h + .m)
→ See examples/Constants.h and examples/Constants.m
Generate at: <rnSdkDir>/ios/MoEngage<featureNameCamel>ReactConstants.h/.m
Rules:
- Always define
kPayload = @"payload"(copy from Cards unless already imported via core) - One
NSString* constper nativeToHybrid event name (omit section if no events) - Event name string values must exactly match the TS
Constants.tsevent names
3.4 Handler (.h + .m)
→ See examples/Handler.h and examples/Handler.m
Generate at: <rnSdkDir>/ios/MoEReactNative<featureNameCamel>Handler.h/.m
Rules:
- Singleton pattern with
dispatch_once(copy from Cards exactly) @property (nonatomic, weak) RCTEventEmitter *eventEmitter— only if event methods exist-(void)initialize:(NSString *)payload— only if event methods exist; sets the sync delegate- Fire-and-forget methods: parse payload with
[MoEngageReactUtils getJSONRepresentation:payload], call[[<iosPluginBridge> sharedInstance] <methodName>:jsonPayload], no return - Promise methods: parse payload, call
[[<iosPluginBridge> sharedInstance] <methodName>:jsonPayload completionHandler:^(NSDictionary* result){ [<iosUtilName> handleDataToReact:result rejecter:reject resolver:resolve]; }] - Event delegate method — generic pattern (
sdk/core,MoEngagePluginBridgeDelegate): implementsendMessageWithEvent:message:. Theeventstring is already the event name — emit directly:- (void)sendMessageWithEvent:(NSString *)event message:(NSDictionary<NSString *,id> *)message { [self.eventEmitter sendEventWithName:event body:message]; } - Event delegate method — typed enum pattern (
sdk/cardsand other non-core modules, e.g.MoEngageCardSyncDelegate): implement the typed delegate method. Use<iosUtilName> fetchSyncType:to map the enum to an event name string, serializedatato JSON, put inupdatedDict[kPayload], emit:
Ask user for the exact delegate protocol name and method signature for unknown modules → add- (void)syncCompleteForEventType:(MoEngageCardsSyncEventType)eventType withData:(NSDictionary<NSString *,id> *)data { NSMutableDictionary *updatedDict = [NSMutableDictionary dictionary]; NSString *eventName = [MoEngageCardsReactUtil fetchSyncType:eventType]; if (eventName && data) { NSError *err; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&err]; if (jsonData) { NSString *strPayload = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; updatedDict[kPayload] = strPayload; [self.eventEmitter sendEventWithName:eventName body:updatedDict]; } else { NSLog(@"Error converting dictionary to string %@", err.localizedDescription); } } }// TODO: verify delegate protocol name. - Fire-and-forget / event trigger methods in Handler: parse payload with
[MoEngageReactUtils getJSONRepresentation:payload], call[[<iosPluginBridge> sharedInstance] <methodName>:jsonPayload]. Example:- (void)getSelfHandledInApps:(NSString *)payload { NSDictionary *jsonPayload = [MoEngageReactUtils getJSONRepresentation:payload]; [[MoEngagePluginBridge sharedInstance] getSelfHandledInApp:jsonPayload]; } - Import
@import MoEngagePlugin<featureNameCamel>;for the plugin-base bridge class - Conform to
<iosDelegateName>in the class extension — only if event methods exist
3.5 Util (.h + .m) (only if promise or event methods exist)
→ See examples/Util.h and examples/Util.m
Do not create a feature-specific Util. Use the existing module-level <iosUtilName> file:
- Path:
<rnSdkDir>/ios/<iosUtilName>.h/.m - Read the existing file first, then add only the missing methods — never recreate it.
Methods to add if not already present:
+fetchSyncType:— maps SDK event type enum → event name constant string- Only for typed enum delegate pattern (non-core modules like cards, geofence, inbox)
- Not needed for
sdk/core— core'ssendMessageWithEvent:message:already receives the event name string directly - One
caseper nativeToHybrid event type enum value
+handleDataToReact:rejecter:resolver:— shared JSON serialization + resolve/reject helper- Used by all promise methods in Handler.m
- On success:
resolver(strPayload); on failure:rejecter(error.code, error.localizedDescription, error)
3.6 Bridge (.h + .mm)
→ See examples/Bridge.h and examples/Bridge.mm
Generate at: <rnSdkDir>/ios/MoEngage<featureNameCamel>Bridge.h/.mm
Rules:
.h: conditional#ifdef RCT_NEW_ARCH_ENABLED— new arch inheritsNativeMoEngage<featureNameCamel>Spec, old arch inheritsRCTBridgeModule; both extendRCTEventEmitter.mm: includehasListenersivarstartObserving/stopObserving/supportedEvents— only if event methods existsupportedEventsreturns array of all nativeToHybrid event name constantsRCT_EXPORT_MODULE()— no argument (module name auto-derived from class name)initialize:— only if event methods exist; setseventEmitter = selfon the handler- Each fire-and-forget:
RCT_EXPORT_METHOD(<name>:(NSString *)payload)→ call[[<iosHandlerName> sharedInstance] <name>:payload]on handler - Each event trigger (hybridToNative side of an event method):
RCT_EXPORT_METHOD(<name>:(NSString *)payload)→ call[[<iosHandlerName> sharedInstance] <name>:payload]on handler — notinitialize:. The handler method calls[[<iosPluginBridge> sharedInstance] <name>:jsonPayload]which triggers the SDK to push the event back via the delegate. - Each promise:
RCT_EXPORT_METHOD(<name>:(NSString *)payload resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)→ delegate to handler - New arch block at bottom:
getTurboModule:returningNativeMoEngage<featureNameCamel>SpecJSI - Add
// TODO: verifycomment if the NativeMoEngageSpec header name is unknown
3.7 Podspec
First check if a podspec already exists:
ls <rnSdkDir>/*.podspec 2>/dev/null
If podspec already exists (adding to an existing module):
- Read the existing podspec
- Find the
s.dependency 'MoEngagePlugin<featureNameCamel>', '...'line - Update the version to
'<ios_plugin_version>' - Do not change anything else
If no podspec exists (new module):
- Copy
sdk/cards/ReactNativeMoEngageCards.podspec, rename toReactNativeMoEngage<featureNameCamel>.podspec s.name→"ReactNativeMoEngage<featureNameCamel>"s.summary/s.description→ update feature names.source_files→"ios/**/*.{h,m,mm,swift}"s.dependency 'MoEngagePlugin<featureNameCamel>', '<ios_plugin_version>'- Keep
s.dependency 'React',s.dependency 'ReactNativeMoEngage', andinstall_modules_dependenciesblock
3.8 Commit
git add <rnSdkDir>/ios/ <rnSdkDir>/*.podspec
git commit -m "<ticketId>: Add React-Native iOS bridge for <featureName>"
Phase 4 — Create / Update Pull Request
git push -u origin feature/<ticketId>-<contractSuffix>
# Check if PR already exists (from Android bridge step):
gh pr list --head feature/<ticketId>-<contractSuffix> --json number,url
If PR already exists (Android bridge was done first): push a new commit to the same branch and add a comment to the PR explaining iOS was added. Do not create a second PR.
If no PR exists (iOS-first flow):
gh pr create \
--title "<ticketId>: Add React-Native iOS bridge for <featureName>" \
--base development \
--body "$(cat <<'EOF'
## Summary
- Adds iOS Objective-C bridge (`<rnSdkDir>/ios/`) for the <featureName> feature
- Bridge delegates all methods to `MoEReactNative<featureNameCamel>Handler` singleton
- New-arch (TurboModule via RCT_NEW_ARCH_ENABLED) + old-arch (RCTBridgeModule) supported
- iOS plugin version: <ios_plugin_version>
## Related PRs
- ios-plugin-base: <ios_plugin_base_pr_url>
## Contract
Branch: `<contract_branch>` in mobile-sdk-contracts
## Methods
| Method | Type |
|---|---|
<table rows from method table>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
Phase 5 — Report & Hand-off
Print:
- PR URL
- Full method table (name, type, iOS delegate)
- All
// TODOitems left for manual verification (delegate protocol name, iOS pod dependency) - List of all files created or modified
Then ask the user:
"iOS bridge for
<featureName>is done (PR:). Would you like to also run the TypeScript layer now ( react-native-ts-implementation)? It needs the same contract branch, plugin version, and both PR URLs."
Codebase Reference Files
Read these before generating the corresponding output — copy copyright headers, naming conventions, and structural patterns exactly.
| What | Codebase path |
|---|---|
| Bridge .h reference | React-Native/sdk/cards/ios/MoEngageCardsBridge.h |
| Bridge .mm reference | React-Native/sdk/cards/ios/MoEngageCardsBridge.mm |
| Handler .h reference | React-Native/sdk/cards/ios/MoEReactNativeCardsHandler.h |
| Handler .m reference | React-Native/sdk/cards/ios/MoEReactNativeCardsHandler.m |
| Constants .h reference | React-Native/sdk/cards/ios/MoEngageCardsReactConstants.h |
| Constants .m reference | React-Native/sdk/cards/ios/MoEngageCardsReactConstants.m |
| Util .h reference | React-Native/sdk/cards/ios/MoEngageCardsReactUtil.h |
| Util .m reference | React-Native/sdk/cards/ios/MoEngageCardsReactUtil.m |
| Podspec reference | React-Native/sdk/cards/ReactNativeMoEngageCards.podspec |
| Core Bridge (event queue pattern) | React-Native/sdk/core/iOS/MoEReactBridge/MoEReactBridge.mm |
| Core Handler (promise pattern) | React-Native/sdk/core/iOS/MoEReactBridge/MoEReactNativeHandler.m |
| MoEngageReactUtils | React-Native/sdk/core/iOS/MoEReactBridge/MoEngageReactUtils.h/.m |
Error Handling Rules
contract_branchnot found in../mobile-sdk-contracts→ stop and tell the usercontractDirnot found injson/hybridToNative/→ list available dirs and askrnSdkDir/ios/already exists → read existing files, add only missing methods- Plugin-base delegate protocol name unknown → add
// TODO: verify delegate protocol nameand continue NativeMoEngage<featureNameCamel>Specheader name unknown → add// TODO: verify TurboModule spec nameand continue- iOS pod dependency name unknown → add
// TODO: verify iOS pod dependencyin podspec and continue - Push fails → report error and local branch name so the user can push manually