Skip to content

refactor(ios)!: port the React Native bridge module to Swift - #298

Draft
kherembourg wants to merge 33 commits into
mainfrom
feat/ios-swift-bridge-spec
Draft

refactor(ios)!: port the React Native bridge module to Swift#298
kherembourg wants to merge 33 commits into
mainfrom
feat/ios-swift-bridge-spec

Conversation

@kherembourg

@kherembourg kherembourg commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Phase 1 and phase 2 of the iOS Objective-C to Swift bridge migration, MOB-466.

Spec: docs/superpowers/specs/2026-09-08-ios-bridge-swift-migration-design.md
Plan: docs/superpowers/plans/2026-09-08-ios-bridge-swift-migration.md (16 tasks; the
"Orchestrator amendments" block at the top records every decision taken during execution)

2086 lines of Objective-C become six Swift files plus a 63-line export shim. No JavaScript API
change
: same module name, same 63 method names, same 60 constants with the same values, same 11
events, same dictionary keys and the same per-field absence policy. Nothing under src/ is touched.

Why the shim still exists

React Native discovers a legacy module through class methods that only RCT_EXTERN_REMAP_MODULE and
RCT_EXTERN_METHOD generate, and Swift cannot emit them. So PurchaselyRN.m survives as
RCT_EXTERN_REMAP_MODULE plus 63 RCT_EXTERN_METHOD lines and the PLYRNLogWarn wrapper
(RCTLogWarn is a variadic macro Swift cannot call).

That shim is parsed as text at registration, not linked. A Swift @objc signature that disagrees
with its RCT_EXTERN_METHOD line produces no compile error and fails at run time in a client app with
"method not found". No build job catches it, so three gates do:

Gate What it locks
BridgeExportContractTests the 63 JS names, the module name, the 63 exported selectors, the 60 constants with their values, the 11 events
BridgeSelectorResolutionTests every one of the 63 shim selectors actually resolves on the class
SerializationContractTests each serializer's key set, value types and per-field absence policy

Both contract gates were written against the Objective-C output before any code moved, and each was
shown failing before it counted — on a removed export, on a renamed selector, on a changed argument
count, and on an emptied export table.

Two selectors change; neither is JS-visible

RCT_EXTERN_REMAP_METHOD is not public in RN 0.86 (RCTBridgeModule.h:310-323), so the shim cannot
remap. restoreAllProducts and silentRestoreAllProducts therefore take selectors whose first segment
is their JS name:

resolve:reject:                    ->  restoreAllProducts:reject:
silentRestoreWithResolve:reject:   ->  silentRestoreAllProducts:reject:

The other 61 were verified byte-for-byte against their Swift @objc(...) annotations, mechanically and
without sampling.

PLYSubscription stays Objective-C, on purpose

This is the one deliberate departure from the plan. PLYSubscription.init(from:) resolves .product
through ProductRepository.shared, which is internal with no injection seam — the SDK's own source
carries #warning("This decoding depends on ProductRepository, which cannot be injected"). No fixture
can be built, and integration_test/ never calls userSubscriptions either, so a port would have been
the only change here with no automated coverage at any level — on the one serializer whose absence
policy types.ts:138-141 documents to clients.

Classes/Hybrid/PLYSubscription+Hybrid.{h,m} therefore remains, reaching the Swift serializers through
a forward declaration. Because an Objective-C message send to a Swift @objc extension method is
runtime dispatch with no link-time reference, @objc on PLYPlan.asDictionary and
PLYProduct.asDictionary is permanent
, and testPlanAndProductRespondToAsDictionarySelector is the
only thing that catches its removal. That was verified by mutation: dropping @objc leaves the build
green and turns that test red.

Follow-up: port it once the SDK gives ProductRepository an injection seam, which its own TODO already
anticipates.

One intentional behaviour change

The hex colour parser is now uniformly stricter on malformed input. The Objective-C used NSScanner,
which returned a colour — often opaque black — for input like "FF00ZZ", "0xFFFF" or "FF 000";
UInt32(_:radix:) returns nil, and a hex-digit guard makes the leading-sign case ("+12345") behave
the same way as the rest instead of being an exception. The path is client-reachable
(map["backgroundColors"]["light"] comes from a JS argument), so a malformed colour now falls back
instead of rendering black.

Everything else is behaviour-preserving, including two things the port had changed and which were put
back: the five array attribute setters (the Objective-C coerced elements in two of them and rejected the
whole array in the other three — user attributes drive audience targeting, so a silently partial array
shows the wrong paywall), and a nil requestId, which omits its key from the event body rather than
sending an empty string.

Also deleted

PurchaselyRN.h, PLYTransitionFactory.swift (its only reason was that Objective-C cannot construct a
PLYTransition), the stale packages/purchasely/ios/Purchasely.xcodeproj that no CI job reads, the
seven +Hybrid serializer pairs, and three dead members (sharedViewController, shouldReopenPaywall,
presentedPresentationViewController).

Verification

Check Result
iOS unit tests 219 passed, 0 failures (83 before this work)
Example app build BUILD SUCCEEDED
Framework layout, USE_FRAMEWORKS=static BUILD SUCCEEDED
JS tests 283 passed, 6 suites
yarn lint / yarn typecheck 0 errors

e2e-ios T1-T30 is the real acceptance criterion — Jest mocks the native module, so it proves nothing
about a text-parsed shim reaching JS.

Release vehicle 6.2.0, not a 6.1.x patch: no client-visible feature, but the whole native layer of
one platform changes.

kherembourg and others added 30 commits September 8, 2026 12:06
Two-phase plan: the Hybrid serialization categories first, then the
2086-line module behind a thin RCT_EXTERN_METHOD shim. Records the
React Native constraints that shape it (the shim is text-parsed, so a
signature mismatch fails at run time), the -import-underlying-module
build fact that makes react-native-purchasely-Bridging-Header.h
load-bearing, and why a TurboModule is a separate decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two reviewers attacked the first draft. Verified and applied:

- The NSNull rule was wrong and would have broken the client contract:
  the bridge omits keys per field and types.ts documents it. Now a
  per-field policy.
- 60 exported constants, not 34. 34 dispatch_async, not 40. 15 Hybrid
  files. Test suites are 535/785 lines, not 330/265 (CLAUDE.md is stale).
  E2E is T1-T30.
- RCT_EXTERN_REMAP_METHOD is not public in RN 0.86, so the 7
  RCT_REMAP_METHOD sites get an explicit @objc(selector:) instead.
- Nil reaches a Swift thunk unchecked in Release (the check is behind
  RCT_DEBUG), so every object parameter must be Optional.
- A Swift enum in [String: Any] is dropped by the bridge: .rawValue
  everywhere, and the snapshots assert value types.
- NSLock is not reentrant and @synchronized is; closePresentation has
  two sequential locked blocks, so scope must be preserved per block.
- Blanket [weak self] would settle the native promise twice; making the
  reject helper static removes the whole question.
- ios/Serialization/ is outside the podspec globs -> ios/Classes/.
- The parity gate uses RCTParseMethodSignature and asserts JS names,
  not RCTModuleMethod + RCTAssert; the Node fallback is cut.
- 81c5a65 was a main-queue module-holder deadlock, not duplicate
  registration.
- sharedViewController, shouldReopenPaywall and
  presentedPresentationViewController are dead: deleted, not ported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Goal 3's swiftinterface citations now come from the plain
arm64-apple-ios-simulator.swiftinterface (PLYPromoOffer exposes only
vendorId/storeOfferId; PLYTransition.init 994, drawer/popin 1009-1010,
PLYDimension 1150, from(screenId:) 756). Confirmed 5 of the 7
RCT_REMAP_METHOD sites already match their JS name, so exactly two
selectors change. Confirmed the 18 reject:with: call sites are the only
instance member the strongly capturing blocks touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 1's serializer key lists now come from the five .m files, including
the note that PLYPlan's five offer* keys are deliberate safe defaults the
port must not try to fix (PLYTagHelper is private and takes an internal
type, so Swift cannot reach it either). Task 4 carries the real hex
parser and the forms it actually accepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two adversarial reviews found 12 blocking errors in the first draft, all
one root cause: Swift written from memory for SDK calls instead of read
off arm64-apple-ios-simulator.swiftinterface. Fixed, each verified:

- requiresMainQueueSetup returns YES (PurchaselyRN.m:1447). The draft
  hardcoded false, violating its own Global Constraint 11.
- start takes 10 arguments beginning with an apiKey string, and its
  resolve segment is named initialized:. The draft invented a 3-argument
  options-dict signature.
- purchaseResultOrdinal returns nil for .none; the draft returned
  non-optional and mapped .none to 1, turning no-purchase into cancelled.
- PLYLogLevel is nested under PLYLogger and set via setLogLevel(_:), not
  an assignable property. PLYAttribute and PLYThemeMode are nested too.
- The legal-basis mapper takes a String and returns
  PLYDataProcessingLegalBasis; PLYLegalBasis does not exist.
- isEligibleForIntroductoryOffer recursed into itself; the SDK method is
  isUserEligibleForIntroductoryOffer and is already @objc, so the wrapper
  is deleted and the one caller repointed.
- No model type has init(); fixtures are decoded from JSON, which also
  gives the populated case the spec requires.
- asDictionary imports as a method, not a property, so the Swift
  replacement is a method and the contract tests stay frozen.
- PLYProduct+Hybrid.m imports PLYPlan+Hybrid.h: phase 1 would not build.
- nonce/timestamp guards are dead code; both keys are always present.
- commitmentInfo guards on count > 0, so it needs !isEmpty, not if let.
- The gate had a tuple-label compile error, and the negative test would
  have orphaned a method body.

Also: Global Constraint 0 (never write an unread SDK symbol) plus a
verified-symbol table, exclusive method ownership (synchronize was in two
tasks), a 60-value constants assertion instead of a types-only one, the
log helper moved to Task 8 to drop a TODO round-trip, the @objc public to
internal cleanup assigned to Task 14, and a Task 16 for CI/E2E acceptance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Snapshots the key sets, the per-field absence policy and the value types
the Objective-C categories produce today, on a populated and a sparse
fixture each, so the Swift port cannot change them silently. The absence
policy is the live hazard: Swift removes a key assigned nil, and types.ts
documents to clients which keys the iOS bridge omits.

Fixtures are decoded from JSON because no SDK model type has a
no-argument initializer.

PLYSubscription is not covered: its init(from:) resolves .product via the
SDK-internal ProductRepository.shared, which is empty and unseedable from
outside the module in a unit test, so decoding always throws
couldntFindProduct. Reported for Task 3.
One branch and one pull request instead of two, per the user's instruction.

PLYSubscription is not ported: its init(from:) needs ProductRepository, which
is internal with no injection seam, so no fixture can be built and no E2E test
covers userSubscriptions either. Porting it would be the only change in this
migration with no automated coverage at any level, on the one serializer whose
absence policy types.ts documents to clients. It stays Objective-C behind a
compile-time dependency the three iOS build jobs check.

Also records the two gate limitations Task 1's review found, so no later task
closes them by relaxing an assertion.
…s and the mappers

An adversarial review of the gate found five holes a wrong port walks through:
the nested plan dictionaries were unpinned, no assertion compared an exact key
set, both billing-plan-type mappers had no coverage at all, and the coalesced
offer defaults were asserted present but never valued.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two FOUNDATION_EXPORT C mappers become @objc static members, since a
free Swift function cannot be @objc, and all six Objective-C call sites
move with them — PLYProduct+Hybrid.m imported that header too.

asDictionary stays a method, not a property, so its call shape is
identical in Objective-C and Swift and the contract tests are untouched.

The intro-offer eligibility category method is deleted rather than
ported: the SDK's own isUserEligibleForIntroductoryOffer is already
@objc, so the one caller now uses it directly.

Two additions beyond the plan snippet, both required to keep frozen
files green without editing them:
- SerializationContractTests.swift (Task 1's gate) calls
  PLYBillingPlanTypeToRNString/FromRNString as top-level Swift
  functions, not as PLYPlan static members. Added thin free-function
  aliases in PLYPlan+Bridge.swift that delegate to the @objc static
  members, so both the Swift gate test and the Objective-C call sites
  share one implementation.
- PLYSubscription+Hybrid.m (frozen per Amendment A2, stays Objective-C)
  called self.plan.asDictionary via the now-deleted PLYPlan+Hybrid.h.
  Added a local @interface PLYPlan (BridgeSerialization) forward
  declaration in that .m file per the orchestrator's seam guidance —
  the Swift extension supplies the method body at link time, avoiding
  the react_native_purchasely-Swift.h import cycle.
PLYProduct, PLYOfferSignature and PLYPresentationPlan move to Swift
extensions under Classes/Serialization/, translated key for key from
their Objective-C categories. PLYOfferSignature's nonce/timestamp
guards were dead code on non-optional values (Constraint 6), so both
keys are unconditional. PLYPresentationPlan.default is a Swift Bool on
the SDK interface, not an enum, so it stays a boxed BOOL rather than
.rawValue as an earlier plan draft called for (Global Constraint 0:
the interface wins).

Amendment A2: PLYSubscription is NOT ported and stays Objective-C —
ProductRepository has no injection seam, so no fixture can exercise it.
PLYSubscription+Hybrid.m now forward-declares PLYPlan and PLYProduct's
asDictionary the same way Task 2 did for PLYPlan, since both are now
Swift extension methods supplied at link time.
The UIViewController (Hybrid) category is deleted, not ported: it has no
caller ([presentation close] is the PLYPresentation protocol method), and
a library that adds a -close selector to every view controller in the
host app is a liability with no user.

One deliberate behaviour change in the hex parser: the Objective-C version
cast scanHexInt:'s result to (void), so a malformed 6-character string such
as #GGGGGG scanned to 0 and painted opaque black. The Swift version returns
nil and the caller falls back. Every well-formed input parses identically.

Adds s.swift_versions to the podspec, now that the pod is Swift-majority.

Per Orchestrator amendment A2, PLYSubscription+Hybrid.{h,m} and a trimmed
Purchasely_Hybrid.h (now importing only PLYSubscription+Hybrid.h) stay in
Classes/Hybrid/ — PLYSubscription is not ported.

Test file: new ios/PurchaselyTests/UIColorPLYHexTests.swift, not an append
to SerializationContractTests.swift as the task step literally said — that
file is a frozen gate outside this task's Files list, and Tasks 2-3 never
touched it either.
…uild-time one

The Task 2 review caught a false claim in my own amendment. An Objective-C
message send to a Swift @objc extension method is runtime dispatch and creates
no link-time reference, and the hand-written forward declaration is never
checked against the Swift symbol. Dropping @objc would keep every build green
and crash the first userSubscriptions call with an unrecognized selector.

So the tie is a selector-presence test, and @objc on the two asDictionary
methods is permanent rather than a phase-1 scaffold.
An Objective-C send to a Swift @objc extension method is runtime dispatch, so
dropping @objc from PLYPlan.asDictionary would keep every build green and
crash the first userSubscriptions call. A selector-presence test is the only
thing that catches it, and two comments claimed the opposite.

Also renames a local that shadowed PLYPlan.period, rejects the leading-sign
hex input the ported parser started accepting, and covers all four channels
of the 8-digit branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reads React Native's own __rct_export__ table and asserts the 63 JS
method names, the module name, the 60 constant keys with numeric values,
and the 11 event names. The shim is parsed as text, so a Swift signature
that disagrees with it fails only at run time in a client app; this turns
that into a CI failure. Verified it fails when an export is removed and
when a selector is renamed.
Done before the implementation moves, so the port of the module runs
under an unchanged Swift test file. XCTestCase, not Swift Testing: a
CocoaPods test spec requires it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sharedViewController's only reader was its own accessor and a test;
shouldReopenPaywall was written once and never read; and
presentedPresentationViewController was written twice and never read.
Deleting them removes the trickiest ownership question of the Swift port
(class-wide mutable storage whose getter recreates a controller after its
setter receives nil) instead of translating it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fix lot tried to prove the seam and could not, which is the useful result:
PurchaselyRN.m still calls asDictionary at 15 sites through the generated Swift
header, so dropping @objc fails the build today rather than passing it. The
runtime-only seam starts at Task 14, which now owns the red-test/green-build
mutation proof.
The export gate locked the 63 JS names but never asserted objcName, so a
changed selector or argument count passed it — which is the one failure the
text-parsed shim cannot catch at build time. Snapshots the Objective-C
selectors now so Task 14 has a before-image.

Also corrects the sibling @objc-is-temporary comment that would have steered
Task 14 into the unrecognized-selector crash A2 exists to prevent, and
restores two assertions the test port had quietly weakened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Named PurchaselyBridge and carrying no export macro until the swap
commit, so React Native never sees two classes claiming the JS module
name Purchasely. Holds the shared state behind a scoped NSLock helper
(NSLock is not reentrant and @synchronized was), the 60 constants, the 11
events, and a static reject helper — static so the 18 closures that use
it capture nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The skeleton implemented the 4-argument PLYUserAttributeDelegate method while
the Objective-C implements only the 5-argument one, so the Swift would have
dropped processingLegalBasis from USER_ATTRIBUTE_SET_LISTENER — a documented
React Native API feature.

Also normalises the exported-selector snapshot to the bare selector so the
gate survives the Task 14 rewrite without being regenerated, which is the one
response that would swallow a real selector change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 9 of the Swift bridge migration plan: PurchaselyRN+Lifecycle.swift
carries start, purchasePerformed, setLogLevel, setThemeMode, userLogin,
userLogout, isAnonymous, getAnonymousUserId, handleDeeplink,
readyToOpenDeeplink, allowDeeplink, allowCampaigns, setLanguage,
setDebugMode, userDidConsumeSubscriptionContent,
revokeDataProcessingConsent (with its mapPurposesFromStrings mapper) and
synchronize, as an extension on the still-unregistered PurchaselyBridge
skeleton.

BridgeLifecycleTests.swift covers the mapper and the three guard
branches (unknown log level, unknown theme mode, nil deeplink) that are
observable without a live SDK; the remaining methods are single SDK
calls left to E2E, per the plan's task shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
React Native's nil check sits behind #if RCT_DEBUG, so in a client Release
build nil passes straight into the Swift thunk. The attributes port declared
21 parameters non-Optional because the Objective-C annotated them _Nonnull —
documentation, not a Release guarantee — and each one would have trapped in a
client app. Selectors are unchanged, so the shim still matches.

Also restores NSLog at the two sites the port had rerouted through RCTLogWarn,
which a Release build filters out, and covers the two numeric traps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port PurchaselyRN.m:972-1240 (Task 11) to
packages/purchasely/ios/PurchaselyRN+Products.swift: purchaseWithPlanVendorId,
restoreAllProducts, silentRestoreAllProducts, allProducts,
productWithIdentifier, planWithIdentifier, userSubscriptions,
userSubscriptionsHistory, setDynamicOffering, getDynamicOfferings,
removeDynamicOffering, clearDynamicOfferings, signPromotionalOffer,
isEligibleForIntroOffer — 14 @objc exported methods on PurchaselyBridge, no
export macro yet (Task 14 renames the class).

Every object-typed parameter is Optional per Global Constraint 4, regardless
of the ObjC _Nonnull annotation; the two REMAP selectors that changed shape
(restoreAllProducts, silentRestoreAllProducts) keep the exact @objc selector
the plan specifies. The promo-offer lookup inside purchaseWithPlanVendorId
and the getDynamicOfferings dictionary mapping are extracted as static
helpers and unit tested; every other method here is a single SDK call and is
left to E2E, per the shared task shape.

BridgeProductsTests.swift: 5 new tests, all failing first against the
skeleton (no `storeOfferId`/`offeringDictionary` members), then passing
after the extension was written. Full suite: 155 tests, 0 failures.

synchronize stays Task 9's; not redeclared here.
Task 12 of the Swift bridge migration plan. Ports PurchaselyRN.m's
preload/display/close/back/BYOS presentation surface, the presentation
helpers (presentationToMap, presentationErrorToMap, closeReasonToRNString,
transition parsing, presentationBuilder), the 5 static members
PurchaselyView.swift calls (zero diff there), webRedemptionBody, and the
3 delegate conformance bodies Task 8 stubbed (eventTriggered,
onUserAttributeSet/onUserAttributeRemoved, webRedemptionCompleted).

Removes Task 8's 4 stub delegate methods from PurchaselyRN.swift so this
extension's real bodies can replace them (Swift rejects the redeclaration
otherwise) — required by the task text ("Replace Task 8's stubs"), not a
drive-by; the Files list named two files, this makes it three.

180 tests green (27 new in BridgePresentationsTests), including the
closePresentation two-block-lock non-deadlock proof.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Objective-C treated the five array attribute setters two different ways —
two coerced each element, three rejected the whole array — and the port
flattened both into a per-element drop, which sets a partial array where the
Objective-C either coerced or set nothing. User attributes drive audience
targeting, so a partial array shows a client the wrong paywall.

Also omits a nil requestId from the presentation event bodies instead of
sending an empty string, restores the log prefixes, and covers
emitPresentationDismissed, whose payload every display() promise resolves from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PurchaselyRN.m drops from 2086 lines to an export shim plus one
non-variadic log wrapper, because RCTLogWarn is a variadic macro Swift
cannot see. PurchaselyRN.h, PLYTransitionFactory.swift (its only reason
was that Objective-C cannot build a PLYTransition) and the stale
Purchasely.xcodeproj are deleted.

Two selectors change and neither is JS-visible: restoreAllProducts and
silentRestoreAllProducts now carry selectors whose first segment is their
JS name, because RCT_EXTERN_REMAP_METHOD is not public in RN 0.86.

Gate verified with teeth twice on the Swift side: removing a shim line
fails the JS-name and count tests, and renaming a selector segment fails
both those and the selector-resolution test.

Added BridgeSelectorResolutionTests.swift (import React resolved fine in
the test target, so no Objective-C fallback was needed) to prove every
shim selector actually resolves on PurchaselyRN via
RCTParseMethodSignature.

Reduced PLYOfferSignature+Bridge.swift, PLYPresentationPlan+Bridge.swift
and UIColor+PLYHex.swift from @objc public to internal now that no
Objective-C caller remains. PLYPlan.asDictionary and
PLYProduct.asDictionary stay @objc public permanently per amendment A2
(PLYSubscription+Hybrid.m's forward declaration is the sole,
runtime-only consumer). Demonstrated the amendment's mutation proof:
dropping @objc from PLYPlan.asDictionary left the build green
(unit-test target and the example app both still compiled and linked)
while SerializationContractTests.testPlanAndProductRespondToAsDictionarySelector
went red; reverted.
The selector-resolution test passed vacuously on an empty export table, the
presentation-dismissed payload was pinned for four of its six keys, and six of
the seven event bodies carrying the requestId omission had no test — including
the ones display() actually resolves from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kherembourg and others added 2 commits September 9, 2026 04:45
Also corrects two stale test line counts that predate this work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bridge sections explained how to add a shim line but never stated the rule
that makes it work: the Swift method needs an explicit @objc(selector:) whose
first segment is the JS name. Also adds the view manager, the second
text-parsed shim in the pod, and replaces the two-file iOS test list with the
real suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kherembourg kherembourg changed the title docs(ios): design and plan for the Objective-C to Swift bridge migration refactor(ios)!: port the React Native bridge module to Swift Sep 9, 2026
A frozen main queue produced only "preload() did not settle", which cost a
long investigation across CI artifacts to localise. A 5-second sample taken
before the kill names the blocked frames directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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