| 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/<featureName> module in the React-Native repo, following the Cards module standard. On completion, asks the user whether to also run react-native-ts-implementation. Do NOT use for Android-only features or JS-only changes.
|
| parameters | [{"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<featureNameCamel> 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_contracts
MOEN-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 identify contractDir
- If no matching directory → list available dirs and ask the user
- For each
.json in json/hybridToNative/<contractDir>/:
- Filename (without
.json) = method name (camelCase)
- File content = input payload shape
- For each
.json in json/nativeToHybrid/<contractDir>/:
- File content = event/response payload shape
- Read all
.proto files in protos/<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:
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>
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* const per nativeToHybrid event name (omit section if no events)
- Event name string values must exactly match the TS
Constants.ts event 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): implement sendMessageWithEvent:message:. The event string 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/cards and other non-core modules, e.g. MoEngageCardSyncDelegate): implement the typed delegate method. Use <iosUtilName> fetchSyncType: to map the enum to an event name string, serialize data to JSON, put in updatedDict[kPayload], emit:
- (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);
}
}
}
Ask user for the exact delegate protocol name and method signature for unknown modules → add // 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's sendMessageWithEvent:message: already receives the event name string directly
- One
case per 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 inherits NativeMoEngage<featureNameCamel>Spec, old arch inherits RCTBridgeModule; both extend RCTEventEmitter
.mm: include hasListeners ivar
startObserving / stopObserving / supportedEvents — only if event methods exist
supportedEvents returns array of all nativeToHybrid event name constants
RCT_EXPORT_MODULE() — no argument (module name auto-derived from class name)
initialize: — only if event methods exist; sets eventEmitter = self on 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 — not initialize:. 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: returning NativeMoEngage<featureNameCamel>SpecJSI
- Add
// TODO: verify comment 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 to ReactNativeMoEngage<featureNameCamel>.podspec
s.name → "ReactNativeMoEngage<featureNameCamel>"
s.summary / s.description → update feature name
s.source_files → "ios/**/*.{h,m,mm,swift}"
s.dependency 'MoEngagePlugin<featureNameCamel>', '<ios_plugin_version>'
- Keep
s.dependency 'React', s.dependency 'ReactNativeMoEngage', and install_modules_dependencies block
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>
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
// TODO items 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: <pr_url>).
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_branch not found in ../mobile-sdk-contracts → stop and tell the user
contractDir not found in json/hybridToNative/ → list available dirs and ask
rnSdkDir/ios/ already exists → read existing files, add only missing methods
- Plugin-base delegate protocol name unknown → add
// TODO: verify delegate protocol name and continue
NativeMoEngage<featureNameCamel>Spec header name unknown → add // TODO: verify TurboModule spec name and continue
- iOS pod dependency name unknown → add
// TODO: verify iOS pod dependency in podspec and continue
- Push fails → report error and local branch name so the user can push manually