iOS Fabric pointer-handler reset leaves the next lone touch non-primary

Offen Anfängerfreundlich
#59,919 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 2 Tagen

@alleneubank arbeitet bereits daran.

Seit 10.11.2026.

  • #59920 von @alleneubank — offen

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
68/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
ios, objective-c, react-native
Bereich
frontend, mobile

Rechercherichtung

Beginne mit packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm, insbesondere mit dem Reset-Pfad um die Zeilen 711–729 sowie den Pfaden zur Registrierung und Deregistrierung primärer Touches um die Zeilen 536–599. Lies den vorgeschlagenen Regressionstest in packages/react-native/React/Tests/TouchHandling/RCTSurfacePointerHandlerTests.mm und führe ihn über das fokussierte gehostete RNTester-Testziel aus. Fertig ist die Arbeit, wenn der Reset den alten Touch mit seiner ursprünglichen Identität und seinem ursprünglichen Primary-Bit abbricht und ein späterer einzelner Touch als primär ausgegeben wird.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

Description

Status: the proposed RNTester XCTest was observed failing on the original handler and passing with the fix (iOS simulator, source-built RNTester at main 38f6e192); what was not checked is listed below.

On iOS Fabric, resetting an active RCTSurfacePointerHandler cancels its current contacts and clears their registry, but retains _primaryTouchPointerId. A subsequent lone touch can therefore be emitted with isPrimary=false even though no other touch remains.

Expected: cancellation preserves the old contact's identity and primary bit, then retires primary ownership. A later lone touch starts with isPrimary=true.

Actual: the old down/cancel are primary, but the next lone down, moves, and up can all be non-primary. This report concerns a deliberately induced active reset; it does not establish how often an unmodified app naturally reaches that state.

Source and current status

All root-cause links below are pinned to React Native v0.86.0, commit a632f9efe24bac8b3a113c78469948f55bde0f5d:

  • Registration grants primary ownership only when _primaryTouchPointerId == -1.
  • Ordinary unregistration clears that owner when removing its touch.
  • Reset cancels and clears contacts without calling unregistration or clearing the owner.

This handler is unchanged in main at 38f6e192c8b0a04ddf93b0f21c394c24217f9692 and latest stable v0.88.1. The fresh source-build and runtime observations are reported below. Bounded public issue/PR searches on 2026-11-04 found no matching report; that is not an exhaustive duplicate check.

The correction is iOS-only. Android's JSPointerDispatcher establishes primary ownership on each ordinary ACTION_DOWN before constructing event state. That source difference does not qualify Android cancellation or recognizer takeover.

Proposed fix against main

Set the existing -1 sentinel after cancellation and registry retirement, outside the nonempty-registry branch. The old cancellation payloads retain their primary bit. Empty or repeated reset also clears a stale owner. No public declaration or mouse/pen routing changes.

diff --git a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm
index 18246804d4..0f51c746f2 100644
--- a/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm
+++ b/packages/react-native/React/Fabric/RCTSurfacePointerHandler.mm
@@ -726,6 +726,8 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act
     _activePointers.clear();
     _identifierPool.reset();
   }
+
+  _primaryTouchPointerId = -1;
 }
 
 - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
Regression test proposal

The complete proposed XCTest uses the real RCTSurfacePointerHandler, TouchEventEmitter, and EventDispatcher. Its assertion observes the emitted down/cancel/down identity and primary bits. The fake UITouch and dispatch listener limit it to handler behavior before queueing. It follows the XCTest/Objective-C++ conventions of RCTViewComponentViewTests.mm. Test destination: packages/react-native/React/Tests/TouchHandling/RCTSurfacePointerHandlerTests.mm. The hosted-target integration through a focused plan in RNTester’s existing scheme below is part of the proposal; target discovery and execution are reported separately.

RCTSurfacePointerHandlerTests.mm
/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#import <React/RCTSurfacePointerHandler.h>
#import <XCTest/XCTest.h>
#import <react/renderer/components/view/TouchEventEmitter.h>
#import <react/renderer/core/EventDispatcher.h>
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>

using namespace facebook::react;

@interface PointerTouch : UITouch
@property (nonatomic, weak) UIView *targetView;
@end

@implementation PointerTouch
- (UIView *)view
{
  return self.targetView;
}
- (UITouchType)type
{
  return UITouchTypeDirect;
}
- (UITouchPhase)phase
{
  return UITouchPhaseBegan;
}
- (CGPoint)locationInView:(UIView *)view
{
  return CGPointMake(20, 20);
}
- (NSTimeInterval)timestamp
{
  return 1;
}
- (CGFloat)force
{
  return 1;
}
- (CGFloat)maximumPossibleForce
{
  return 1;
}
- (CGFloat)majorRadius
{
  return 1;
}
- (CGFloat)altitudeAngle
{
  return M_PI_2;
}
- (CGFloat)azimuthAngleInView:(UIView *)view
{
  return 0;
}
@end

@interface PointerTargetView : UIView
@property (nonatomic) SharedTouchEventEmitter emitter;
@end

@implementation PointerTargetView
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
  return _emitter;
}
@end

struct PointerReceipt {
  std::string type;
  int pointerId;
  bool isPrimary;
};

@interface RCTSurfacePointerHandlerTests : XCTestCase
@end

@implementation RCTSurfacePointerHandlerTests

- (void)testLoneTouchAfterActiveResetIsPrimary
{
  std::vector<PointerReceipt> receipts;
  RuntimeScheduler scheduler([](auto &&) {});
  EventQueueProcessor processor([](auto &, auto *, auto &, auto, auto &, auto) {}, [](auto &) {}, [](auto &) {}, {});
  auto dispatcher = std::make_shared<EventDispatcher>(
      processor,
      std::make_unique<EventBeat>(std::make_shared<EventBeat::OwnerBox>(), scheduler),
      [](auto &) {},
      std::weak_ptr<EventLogger>{});
  auto listener = std::make_shared<EventListener>([&receipts](const RawEvent &event) {
    const auto &pointer = static_cast<const PointerEvent &>(*event.eventPayload);
    receipts.push_back({event.type, pointer.pointerId, pointer.isPrimary});
    return true;
  });
  dispatcher->addListener(listener);

  PointerTargetView *target = [[PointerTargetView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
  target.emitter = std::make_shared<TouchEventEmitter>(nullptr, dispatcher);
  UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
  UIViewController *controller = [UIViewController new];
  controller.view = target;
  window.rootViewController = controller;
  window.hidden = NO;
  RCTSurfacePointerHandler *handler = [RCTSurfacePointerHandler new];
  [handler attachToView:target];

  @autoreleasepool {
    PointerTouch *touch = [PointerTouch new];
    touch.targetView = target;
    [handler touchesBegan:[NSSet setWithObject:touch] withEvent:nil];
    [handler reset];
  }

  PointerTouch *next = [PointerTouch new];
  next.targetView = target;
  [handler touchesBegan:[NSSet setWithObject:next] withEvent:nil];

  XCTAssertEqual(receipts.size(), 3u);
  if (receipts.size() == 3) {
    XCTAssertTrue(receipts[2].isPrimary);
    XCTAssertEqual(receipts[0].type, "topPointerDown");
    XCTAssertTrue(receipts[0].isPrimary);
    XCTAssertEqual(receipts[1].type, "topPointerCancel");
    XCTAssertTrue(receipts[1].isPrimary);
    XCTAssertEqual(receipts[1].pointerId, receipts[0].pointerId);
    XCTAssertEqual(receipts[2].type, "topPointerDown");
  }

  [handler reset];
  [handler detachFromView:target];
  dispatcher->removeListener(listener);
  window.hidden = YES;
}

@end
Dedicated app-hosted RNTester test-target integration
diff --git a/packages/rn-tester/Podfile b/packages/rn-tester/Podfile
index 37fce7a19e..add4632149 100644
--- a/packages/rn-tester/Podfile
+++ b/packages/rn-tester/Podfile
@@ -60,6 +60,10 @@ end
 
 target 'RNTester' do
   pods('RNTester')
+
+  target 'RNTesterPointerTests' do
+    inherit! :search_paths
+  end
 end
 
 target 'RNTesterUnitTests' do
diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
index adc9a277d0..d8824cbe9d 100644
--- a/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
+++ b/packages/rn-tester/RNTesterPods.xcodeproj/project.pbxproj
@@ -9,9 +9,11 @@
 /* Begin PBXBuildFile section */
 		0EA618032BE537D3001875EF /* RNTesterBundle.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 0EA618022BE537D3001875EF /* RNTesterBundle.bundle */; };
 		13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
+		2739B3EDD893CA35F4F24F84 /* RCTSurfacePointerHandlerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = B997F112EE69706F065B2DE9 /* RCTSurfacePointerHandlerTests.mm */; };
 		2DDEF0101F84BF7B00DBDF73 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */; };
 		383889DA23A7398900D06C3E /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */; };
 		3D2AFAF51D646CF80089D1A3 /* [email protected] in Resources */ = {isa = PBXBuildFile; fileRef = 3D2AFAF41D646CF80089D1A3 /* [email protected] */; };
+		43C32951B62BA32FD11957E8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAF463989F68EF327A20F4B9 /* Foundation.framework */; };
 		46C0FD761B0B9B1E8662B759 /* libPods-RNTesterUnitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B312B0EEE90BA411618B015 /* libPods-RNTesterUnitTests.a */; };
 		5C60EB1C226440DB0018C04F /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5C60EB1B226440DB0018C04F /* AppDelegate.mm */; };
 		602160B9478DAFF2BAB56595 /* RCTUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */; };
@@ -22,6 +24,7 @@
 		A975CA6C2C05EADF0043F72A /* RCTNetworkTaskTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */; };
 		C175B6D9ED9336FB66637943 /* libPods-RNTester.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */; };
 		CD10C7A5290BD4EB0033E1ED /* RCTEventEmitterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */; };
+		D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */; };
 		E62F11832A5C6580000BF1C8 /* FlexibleSizeExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27F441E81BEBE5030039B79C /* FlexibleSizeExampleView.mm */; };
 		E62F11842A5C6584000BF1C8 /* UpdatePropertiesExampleView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272E6B3C1BEA849E001FCF37 /* UpdatePropertiesExampleView.mm */; };
 		E7DB20D122B2BAA6005AC45F /* RCTBundleURLProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20A922B2BAA3005AC45F /* RCTBundleURLProviderTests.m */; };
@@ -45,15 +48,21 @@
 		E7DB20EC22B2BAA6005AC45F /* RCTMultipartStreamReaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20CF22B2BAA5005AC45F /* RCTMultipartStreamReaderTests.m */; };
 		E7DB20ED22B2BAA6005AC45F /* RCTURLUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7DB20D022B2BAA5005AC45F /* RCTURLUtilsTests.m */; };
 		E7DB213122B2C649005AC45F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB213022B2C649005AC45F /* JavaScriptCore.framework */; };
-		E7DB216722B2F69F005AC45F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB213022B2C649005AC45F /* JavaScriptCore.framework */; };
-		E7DB218C22B41FCD005AC45F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB218B22B41FCD005AC45F /* XCTest.framework */; };
 		F0D621C32BBB9E38005960AC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */; };
 		F1A0B1C23D4E5F6071829302 /* RCTTurboModuleArrayBufferTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */; };
 		F1A0B1C23D4E5F6071829322 /* RCTTurboModuleStringConversionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */; };
-		D1A60AD11B9247A080734F02 /* RCTDevLoadingViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */; };
+		E7DB216722B2F69F005AC45F /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB213022B2C649005AC45F /* JavaScriptCore.framework */; };
+		E7DB218C22B41FCD005AC45F /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7DB218B22B41FCD005AC45F /* XCTest.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
+		771A6F8445BB60E7DAC45233 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
+			remoteInfo = RNTester;
+		};
 		ADA4EF572546F3F8000B7E75 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
@@ -83,6 +92,7 @@
 		2B312B0EEE90BA411618B015 /* libPods-RNTesterUnitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTesterUnitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		2DDEF00F1F84BF7B00DBDF73 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNTester/Images.xcassets; sourceTree = "<group>"; };
 		383889D923A7398900D06C3E /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = "<group>"; };
+		3C3965CAFF5C696F221DF404 /* RNTesterPointerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNTesterPointerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		3D2AFAF41D646CF80089D1A3 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "[email protected]"; path = "RNTester/[email protected]"; sourceTree = "<group>"; };
 		3FF60722627F93D8F62FA1E3 /* Pods-RNTesterUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.release.xcconfig"; path = "Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.release.xcconfig"; sourceTree = "<group>"; };
 		4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNTester.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -95,9 +105,11 @@
 		832F45BA2A8A6E1F0097B4E6 /* SwiftTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SwiftTest.swift; path = RNTester/SwiftTest.swift; sourceTree = "<group>"; };
 		A975CA6B2C05EADE0043F72A /* RCTNetworkTaskTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkTaskTests.m; sourceTree = "<group>"; };
 		AC474BFB29BBD4A1002BDAED /* RNTester.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = RNTester.xctestplan; path = RNTester/RNTester.xctestplan; sourceTree = "<group>"; };
+		B997F112EE69706F065B2DE9 /* RCTSurfacePointerHandlerTests.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = RCTSurfacePointerHandlerTests.mm; path = "../react-native/React/Tests/TouchHandling/RCTSurfacePointerHandlerTests.mm"; sourceTree = "<group>"; };
 		CA59C9994B1822826D8983F0 /* Pods-RNTester.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester.debug.xcconfig"; path = "Target Support Files/Pods-RNTester/Pods-RNTester.debug.xcconfig"; sourceTree = "<group>"; };
 		CD10C7A4290BD4EB0033E1ED /* RCTEventEmitterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventEmitterTests.m; sourceTree = "<group>"; };
 		D134EB89DD98253FCF879A47 /* Pods-RNTesterUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTesterUnitTests.debug.xcconfig"; path = "Target Support Files/Pods-RNTesterUnitTests/Pods-RNTesterUnitTests.debug.xcconfig"; sourceTree = "<group>"; };
+		D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingViewTests.m; sourceTree = "<group>"; };
 		D5D13FB1445C5C19639921CC /* RCTUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUtilsTests.m; sourceTree = "<group>"; };
 		E771AEEA22B44E3100EA1189 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNTester/Info.plist; sourceTree = "<group>"; };
 		E7DB209F22B2BA84005AC45F /* RNTesterUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNTesterUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -143,15 +155,23 @@
 		E7DB211A22B2BD53005AC45F /* libReact-RCTVibration.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libReact-RCTVibration.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		E7DB212222B2BD53005AC45F /* libyoga.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libyoga.a; sourceTree = BUILT_PRODUCTS_DIR; };
 		E7DB213022B2C649005AC45F /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
-		E7DB215722B2F332005AC45F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		E7DB218B22B41FCD005AC45F /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = XCTest.framework; sourceTree = DEVELOPER_DIR; };
 		F0D621C22BBB9E38005960AC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
 		F1A0B1C23D4E5F6071829301 /* RCTTurboModuleArrayBufferTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleArrayBufferTests.mm; sourceTree = "<group>"; };
 		F1A0B1C23D4E5F6071829321 /* RCTTurboModuleStringConversionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTTurboModuleStringConversionTests.mm; sourceTree = "<group>"; };
-		D1A60AD11B9247A080734F01 /* RCTDevLoadingViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTDevLoadingViewTests.m; sourceTree = "<group>"; };
+		FAF463989F68EF327A20F4B9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+		E7DB215722B2F332005AC45F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		106076F774E7039684DC62C2 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				43C32951B62BA32FD11957E8 /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -240,6 +260,7 @@
 				E7DB212222B2BD53005AC45F /* libyoga.a */,
 				4C706D402EE4AF9BE838CBA9 /* libPods-RNTester.a */,
 				2B312B0EEE90BA411618B015 /* libPods-RNTesterUnitTests.a */,
+				85355AD432A16512E101E918 /* iOS */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -260,6 +281,7 @@
 				83CBBA001A601CBA00E9B192 /* Products */,
 				2DE7E7D81FB2A4F3009E225D /* Frameworks */,
 				E23BD6487B06BD71F1A86914 /* Pods */,
+				B997F112EE69706F065B2DE9 /* RCTSurfacePointerHandlerTests.mm */,
 			);
 			indentWidth = 2;
 			sourceTree = "<group>";
@@ -271,10 +293,19 @@
 			children = (
 				13B07F961A680F5B00A75B9A /* RNTester.app */,
 				E7DB209F22B2BA84005AC45F /* RNTesterUnitTests.xctest */,
+				3C3965CAFF5C696F221DF404 /* RNTesterPointerTests.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
 		};
+		85355AD432A16512E101E918 /* iOS */ = {
+			isa = PBXGroup;
+			children = (
+				FAF463989F68EF327A20F4B9 /* Foundation.framework */,
+			);
+			name = iOS;
+			sourceTree = "<group>";
+		};
 		E23BD6487B06BD71F1A86914 /* Pods */ = {
 			isa = PBXGroup;
 			children = (
@@ -325,6 +356,24 @@
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
+		0497DB7F2CEA00B0FE0F8B15 /* RNTesterPointerTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 86DB10AE554725936B24662D /* Build configuration list for PBXNativeTarget "RNTesterPointerTests" */;
+			buildPhases = (
+				D4CA4483E53D4A2C4DED9F29 /* Sources */,
+				106076F774E7039684DC62C2 /* Frameworks */,
+				69C3A65F4B7A44482BF7AA24 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				9BCA55DF6B334CDE241F651C /* PBXTargetDependency */,
+			);
+			name = RNTesterPointerTests;
+			productName = RNTesterPointerTests;
+			productReference = 3C3965CAFF5C696F221DF404 /* RNTesterPointerTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		13B07F861A680F5B00A75B9A /* RNTester */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "RNTester" */;
@@ -400,6 +449,7 @@
 			targets = (
 				13B07F861A680F5B00A75B9A /* RNTester */,
 				E7DB209E22B2BA84005AC45F /* RNTesterUnitTests */,
+				0497DB7F2CEA00B0FE0F8B15 /* RNTesterPointerTests */,
 			);
 		};
 /* End PBXProject section */
@@ -417,6 +467,13 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		69C3A65F4B7A44482BF7AA24 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		E7DB209D22B2BA84005AC45F /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -606,6 +663,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		D4CA4483E53D4A2C4DED9F29 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				2739B3EDD893CA35F4F24F84 /* RCTSurfacePointerHandlerTests.mm in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		E7DB209B22B2BA84005AC45F /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -643,6 +708,12 @@
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
+		9BCA55DF6B334CDE241F651C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = RNTester;
+			target = 13B07F861A680F5B00A75B9A /* RNTester */;
+			targetProxy = 771A6F8445BB60E7DAC45233 /* PBXContainerItemProxy */;
+		};
 		ADA4EF582546F3F8000B7E75 /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			target = 13B07F861A680F5B00A75B9A /* RNTester */;
@@ -731,6 +802,34 @@
 			};
 			name = Release;
 		};
+		22E3485DAB8359FC4A5FB564 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"RN_BUILDING=1",
+				);
+				GENERATE_INFOPLIST_FILE = YES;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"${PODS_ROOT}/Headers/Private/Yoga",
+				);
+				IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.meta.RNTesterPointerTests;
+				SDKROOT = iphoneos;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RNTester.app/RNTester";
+			};
+			name = Debug;
+		};
 		83CBBA201A601CBA00E9B192 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -925,6 +1024,35 @@
 			};
 			name = Release;
 		};
+		8D34CE2CC67497DA25513358 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CODE_SIGN_STYLE = Automatic;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"$(inherited)",
+					"RN_BUILDING=1",
+				);
+				GENERATE_INFOPLIST_FILE = YES;
+				HEADER_SEARCH_PATHS = (
+					"$(inherited)",
+					"${PODS_ROOT}/Headers/Private/Yoga",
+				);
+				IPHONEOS_DEPLOYMENT_TARGET = 15.1;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.meta.RNTesterPointerTests;
+				SDKROOT = iphoneos;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RNTester.app/RNTester";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
 		E7DB20A722B2BA84005AC45F /* Debug */ = {
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = D134EB89DD98253FCF879A47 /* Pods-RNTesterUnitTests.debug.xcconfig */;
@@ -1022,6 +1150,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		86DB10AE554725936B24662D /* Build configuration list for PBXNativeTarget "RNTesterPointerTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				8D34CE2CC67497DA25513358 /* Release */,
+				22E3485DAB8359FC4A5FB564 /* Debug */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		E7DB20A622B2BA84005AC45F /* Build configuration list for PBXNativeTarget "RNTesterUnitTests" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
diff --git a/packages/rn-tester/RNTester/RNTesterPointerTests.xctestplan b/packages/rn-tester/RNTester/RNTesterPointerTests.xctestplan
new file mode 100644
index 0000000000..4043f68c9d
--- /dev/null
+++ b/packages/rn-tester/RNTester/RNTesterPointerTests.xctestplan
@@ -0,0 +1,39 @@
+{
+  "configurations": [
+    {
+      "id": "1D5134C7-450A-44B6-A8B1-13CD24FD50B9",
+      "name": "Address Sanitizer",
+      "options": {
+        "addressSanitizer": {
+          "detectStackUseAfterReturn": true,
+          "enabled": true
+        }
+      }
+    }
+  ],
+  "defaultOptions": {
+    "codeCoverage": false,
+    "environmentVariableEntries": [
+      {
+        "key": "CI_USE_PACKAGER",
+        "value": "1"
+      }
+    ],
+    "targetForVariableExpansion": {
+      "containerPath": "container:RNTesterPods.xcodeproj",
+      "identifier": "13B07F861A680F5B00A75B9A",
+      "name": "RNTester"
+    },
+    "undefinedBehaviorSanitizerEnabled": true
+  },
+  "testTargets": [
+    {
+      "target": {
+        "containerPath": "container:RNTesterPods.xcodeproj",
+        "identifier": "0497DB7F2CEA00B0FE0F8B15",
+        "name": "RNTesterPointerTests"
+      }
+    }
+  ],
+  "version": 1
+}
diff --git a/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme b/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme
index 417f850440..f206a34937 100644
--- a/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme
+++ b/packages/rn-tester/RNTesterPods.xcodeproj/xcshareddata/xcschemes/RNTester.xcscheme
@@ -32,6 +32,9 @@
             reference = "container:RNTester/RNTester.xctestplan"
             default = "YES">
          </TestPlanReference>
+         <TestPlanReference
+            reference = "container:RNTester/RNTesterPointerTests.xctestplan">
+         </TestPlanReference>
       </TestPlans>
    </TestAction>
    <LaunchAction
Steps to reproduce

The failing reproducer is an iOS RNTesterPlayground change plus one RNTester-native reset button rendered inside Playground. The complete patch is below. JavaScript alone has no API for resetting this native handler.

  1. Check out RN main 38f6e192c8b0a04ddf93b0f21c394c24217f9692 and apply only this reproducer patch. Its AppDelegate enables pointer dispatch before React starts. Use v0.88.1 for the latest-stable comparison.
  2. Run yarn install and yarn --cwd packages/react-native-codegen build. Install the repository's bundled CocoaPods dependencies. In packages/rn-tester, use RCT_USE_PREBUILT_RNCORE=0 bundle exec pod install and build RNTester from the generated workspace. Run yarn start to serve RNTester over Metro. On main, resolve the checked-in Hermes-version mismatch with bundle exec pod update hermes-engine --no-repo-update, as RN's CI does.
  3. Open Playground. Confirm an ordinary lone down/move/up is primary, lift it and clear the monitor.
  4. Hold one genuine finger on the blue view. With a second finger press the native Reset pointers button. Its touch-down action finds the real RCTSurfacePointerHandler, tells UIKit to ignore later callbacks for contacts retired by the reset, then invokes reset. The monitor should receive the old primary down/cancel. The button status alone does not prove the blue contact was active; verify its primary down/cancel in the monitor.
  5. Lift both fingers, then start, move and lift one new lone contact. Press Show events. Expected: the old down/cancel remain primary and the new lone down/moves/up are primary. On the affected handler, the stale owner can make that new lifecycle non-primary. Any registry inconsistency, missing cancellation, or absent ordinary control invalidates that trial.
  6. Apply the native production correction in the RN source checkout, rebuild and repeat. A JS reload or prebuilt core cannot exercise this Objective-C++ change.

The reset is deliberate fault injection in the test app. The button uses RNTester's native view-manager mechanism and is mounted only as part of Playground; it is absent from neighboring screens. The experiment does not establish natural production reset frequency. The monitor retains at most 16 phase groups, folding consecutive moves with the same identity and primary bit into a count. The fresh-run observations below state whether this exact input sequence has been executed.

Complete failing RNTesterPlayground and native reset-button patch
diff --git a/packages/rn-tester/RNTester/AppDelegate.mm b/packages/rn-tester/RNTester/AppDelegate.mm
index 8f885bc966..cd7150ecb7 100644
--- a/packages/rn-tester/RNTester/AppDelegate.mm
+++ b/packages/rn-tester/RNTester/AppDelegate.mm
@@ -7,6 +7,8 @@
 
 #import "AppDelegate.h"
 
+#import <React/RCTConstants.h>
+
 #if !TARGET_OS_TV
 #import <React/RCTPushNotificationManager.h>
 #import <UserNotifications/UserNotifications.h>
@@ -21,6 +23,7 @@
 
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
+  RCTSetDispatchW3CPointerEvents(YES);
 #if !TARGET_OS_TV
   [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self];
 #endif
diff --git a/packages/rn-tester/RNTester/SceneDelegate.mm b/packages/rn-tester/RNTester/SceneDelegate.mm
index bd9635151c..c8e523f2c1 100644
--- a/packages/rn-tester/RNTester/SceneDelegate.mm
+++ b/packages/rn-tester/RNTester/SceneDelegate.mm
@@ -13,6 +13,8 @@
 #import <React/RCTBundleURLProvider.h>
 #import <React/RCTDefines.h>
 #import <React/RCTLinkingManager.h>
+#import <React/RCTSurfacePointerHandler.h>
+#import <React/RCTViewManager.h>
 #import <ReactCommon/RCTSampleTurboModule.h>
 #import <ReactCommon/RCTTurboModuleManager.h>
 
@@ -31,6 +33,71 @@
 
 static NSString *const kBundlePath = @"js/RNTesterApp.ios";
 
+@interface RNTPointerResetButton : UIButton
+@end
+
+@implementation RNTPointerResetButton
+
+- (instancetype)initWithFrame:(CGRect)frame
+{
+  if ((self = [super initWithFrame:frame])) {
+    UIButtonConfiguration *configuration = [UIButtonConfiguration plainButtonConfiguration];
+    configuration.title = @"Reset pointers";
+    self.configuration = configuration;
+    self.backgroundColor = UIColor.systemBackgroundColor;
+    self.isAccessibilityElement = YES;
+    self.accessibilityIdentifier = @"reset-pointer-handler";
+    self.accessibilityLabel = configuration.title;
+    [self addTarget:self action:@selector(resetPointers:event:) forControlEvents:UIControlEventTouchDown];
+  }
+  return self;
+}
+
+- (void)resetPointers:(UIButton *)sender event:(UIEvent *)event
+{
+  RCTSurfacePointerHandler *handler = nil;
+  for (UITouch *touch in event.allTouches) {
+    UIView *view = touch.view;
+    for (NSUInteger depth = 0; view != nil && depth < 64; depth++, view = view.superview) {
+      for (UIGestureRecognizer *recognizer in view.gestureRecognizers) {
+        if ([recognizer isKindOfClass:RCTSurfacePointerHandler.class]) {
+          handler = (RCTSurfacePointerHandler *)recognizer;
+          // Reset retires this contact; UIKit must not send its later move/up to the handler.
+          [handler ignoreTouch:touch forEvent:event];
+        }
+      }
+    }
+  }
+  if (handler != nil) {
+    [handler reset];
+  }
+  UIButtonConfiguration *configuration = sender.configuration;
+  configuration.title = handler != nil ? @"Reset performed" : @"No active pointer";
+  sender.configuration = configuration;
+  sender.accessibilityLabel = configuration.title;
+}
+
+@end
+
+@interface RNTPointerResetButtonManager : RCTViewManager
+@end
+
+@implementation RNTPointerResetButtonManager
+
+RCT_EXPORT_MODULE(RNTPointerResetButton)
+
++ (BOOL)requiresMainQueueSetup
+{
+  return YES;
+}
+
+- (UIView *)view
+{
+  return [[RNTPointerResetButton alloc] initWithFrame:CGRectZero];
+}
+
+@end
+
 @implementation SceneDelegate
 
 - (NSDictionary *)prepareInitialProps
diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
index 8e6b10479d..a2b0c077db 100644
--- a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
+++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js
@@ -9,17 +9,94 @@
  */
 
 import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
+import type {HostComponent, PointerEvent, ViewProps} from 'react-native';
 
 import RNTesterText from '../../components/RNTesterText';
 import * as React from 'react';
-import {StyleSheet, View} from 'react-native';
+import {
+  Button,
+  Platform,
+  StyleSheet,
+  View,
+  requireNativeComponent,
+} from 'react-native';
+
+const NativeResetButton: ?HostComponent<ViewProps> =
+  Platform.OS === 'ios'
+    ? requireNativeComponent<ViewProps>('RNTPointerResetButton')
+    : null;
+
+type Phase = 'down' | 'move' | 'up' | 'cancel';
+type Receipt = {
+  phase: Phase,
+  id: number,
+  primary: boolean,
+  count: number,
+};
+
+function Playground(): React.Node {
+  const rows = React.useRef<Array<Receipt>>([]);
+  const [summary, setSummary] = React.useState('No events recorded.');
+
+  function record(phase: Phase, event: PointerEvent) {
+    const {pointerId, isPrimary} = event.nativeEvent;
+    const previous = rows.current[rows.current.length - 1];
+    if (
+      phase === 'move' &&
+      previous?.phase === 'move' &&
+      previous.id === pointerId &&
+      previous.primary === isPrimary
+    ) {
+      previous.count += 1;
+    } else if (rows.current.length < 16) {
+      rows.current.push({phase, id: pointerId, primary: isPrimary, count: 1});
+    }
+  }
 
-function Playground() {
   return (
     <View style={styles.container}>
       <RNTesterText>
-        Edit "RNTesterPlayground.js" to change this file
+        iOS: hold one finger here, then press Reset pointers with another
+        finger. Lift both, then start a lone contact. Its down, moves and up
+        should be primary.
       </RNTesterText>
+      <View
+        collapsable={false}
+        testID="pointer-target"
+        onPointerDown={event => record('down', event)}
+        onPointerMove={event => record('move', event)}
+        onPointerUp={event => record('up', event)}
+        onPointerCancel={event => record('cancel', event)}
+        style={styles.target}
+      />
+      {NativeResetButton != null ? (
+        <NativeResetButton
+          testID="reset-pointer-handler"
+          style={styles.reset}
+        />
+      ) : null}
+      <Button
+        title="Show events"
+        onPress={() => {
+          console.log(`Pointer reset: ${JSON.stringify(rows.current)}`);
+          setSummary(
+            rows.current
+              .map(
+                row =>
+                  `${row.phase} id=${row.id} primary=${row.primary} count=${row.count}`,
+              )
+              .join('\n'),
+          );
+        }}
+      />
+      <Button
+        title="Clear after lifting all fingers"
+        onPress={() => {
+          rows.current = [];
+          setSummary('No events recorded.');
+        }}
+      />
+      <RNTesterText testID="pointer-results">{summary}</RNTesterText>
     </View>
   );
 }
@@ -28,11 +105,21 @@ const styles = StyleSheet.create({
   container: {
     padding: 10,
   },
+  reset: {
+    height: 44,
+    marginBottom: 16,
+  },
+  target: {
+    height: 300,
+    marginVertical: 16,
+    backgroundColor: 'skyblue',
+  },
 });
 
 export default {
   title: 'Playground',
   name: 'playground',
-  description: 'Test out new features and ideas.',
+  description:
+    'Primary touch ownership after an active iOS pointer-handler reset.',
   render: (): React.Node => <Playground />,
 } as RNTesterModuleExample;
React Native Version

0.88.1 (source-built RNTester baseline). Fresh runtime qualification is listed below. The fix/test patch targets pinned main.

Affected Platforms

Runtime - iOS

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

Captured by running npx @react-native-community/cli info from the v0.88.1 RNTester checkout. Tool paths are redacted; all version/configuration fields are preserved. The CLI reports several source-workspace packages as Not Found and architecture booleans as false. Source identity and the actual build are recorded separately; these scanner fields are not runtime evidence. The global CocoaPods binary reported by the CLI differs from the repository-bundled CocoaPods 1.14.2 used for the source build.

System:
  OS: macOS 27.2
  CPU: (16) arm64 Apple M3 Max
  Memory: 27.85 GB / 128.00 GB
  Shell:
    version: 5.9.2
    path: [redacted]
Binaries:
  Node:
    version: 26.12.1
    path: [redacted]
  Yarn:
    version: 1.22.22
    path: [redacted]
  npm:
    version: 11.20.0
    path: [redacted]
  Watchman:
    version: 2026.07.27.00
    path: [redacted]
Managers:
  CocoaPods:
    version: 1.17.0
    path: [redacted]
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.2
      - iOS 27.2
      - macOS 27.2
      - tvOS 27.2
      - visionOS 27.2
      - watchOS 27.2
  Android SDK:
    Android NDK: 27.1.12297006
IDEs:
  Android Studio: 2025.3 AI-253.29346.138.2531.14876573
  Xcode:
    version: 27.2/27B52
    path: [redacted]
Languages:
  Java:
    version: 17.0.20
    path: [redacted]
  Ruby:
    version: 3.2.2
    path: [redacted]
npmPackages:
  "@react-native-community/cli": Not Found
  react: Not Found
  react-native: Not Found
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: false
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: true
Stacktrace or Logs
Test Plan

Fresh runtime evidence on 2026-11-05 UTC: source-built RNTester v0.88.1 (c30b7f86850d47422637a64c70508e7411dbd8ea), iOS 27.2 arm64 simulator, with RCT_USE_PREBUILT_RNCORE=0. The failing Playground/native-view patch is present in both variants; only the production handler correction differs. Genuine AXe Indigo input holds the blue contact, presses the native reset button with a second finger, releases both, then starts a new lone drag. Each native arm waits for the unchanged three-free-core admission guard. This is functional evidence, not a performance measurement.

Native source variant Ordinary lone control Active reset followed by new lone contact
Unpatched v0.88.1 id=2, down=1/move=40/up=1; all primary=true old id=2: down=1/cancel=1, primary=true; new id=3: down=1/move=40/up=1, all primary=false
With handler correction id=2, down=1/move=40/up=1; all primary=true old id=2: down=1/cancel=1, primary=true; new id=3: down=1/move=40/up=1, all primary=true

The native control reports Reset performed. The old monitored down/cancel and complete new lifecycle qualify the trial; button status alone does not establish that the blue contact was active. There is no JS-synthesized pointer event. This deliberate native fault injection does not establish natural production reset frequency. The reset control is part of Playground's native view layout and is absent on Components; the author drove leaving and returning. Both native variants build; the current JavaScript passes RN ESLint with zero warnings, Prettier, and a real stable iOS Metro bundle. Native source passes the pinned clang-format check.

Native XCTest on pinned main (2026-11-05 UTC): the supplied RCTSurfacePointerHandlerTests/testLoneTouchAfterActiveResetIsPrimary now executes through Xcode's standard RNTester scheme, using a focused RNTesterPointerTests test plan and the real RNTester.app host. The focused plan preserves the existing default test plan and its RNTesterUnitTests target. Source base: 38f6e192c8b0a04ddf93b0f21c394c24217f9692; destination: a newly created, Xcode-visible iPhone 17 Pro / iOS 27.2 arm64 simulator in the default device set. React Native was built from source with RCT_USE_PREBUILT_RNCORE=0. The plan retains Address Sanitizer and Undefined Behavior Sanitizer.

Production handler build-for-testing test-without-building
Original main Pass, exit 0 Red: exit 65; 1 test, 1 failure at XCTAssertTrue(receipts[2].isPrimary) (line 125); no other assertion failed
With the supplied reset correction Pass, exit 0 Green: exit 0; 1 test, 0 failures

The fixture's SHA-256 is identical in both arms. Only _primaryTouchPointerId = -1 at the end of reset differs in production code. There is no repetition configured in the focused plan, and each result bundle reports exactly one test. Each native arm passed the three-free-core admission guard; builds used at most three jobs, with one native build at a time.

Commands below were run from the source checkout root. SIMULATOR_UDID names the owned default-set simulator; DERIVED_DATA and RESULT_BUNDLE stand for its lane-owned output paths (a distinct result bundle for each arm). A lane-owned Metro server served this checkout on the initially free port 8081, started from packages/rn-tester with node ../react-native/cli.js start --port 8081 --no-interactive --max-workers 1.

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
export RCT_SKIP_CACHES=1
SKIP_BUNDLING=1 xcodebuild build-for-testing \
  -workspace packages/rn-tester/RNTesterPods.xcworkspace \
  -scheme RNTester -testPlan RNTesterPointerTests \
  -configuration Debug -sdk iphonesimulator \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -derivedDataPath "$DERIVED_DATA" -jobs 3 \
  -only-testing:RNTesterPointerTests/RCTSurfacePointerHandlerTests/testLoneTouchAfterActiveResetIsPrimary \
  ONLY_ACTIVE_ARCH=YES ARCHS=arm64 CODE_SIGNING_ALLOWED=NO
xcodebuild test-without-building \
  -workspace packages/rn-tester/RNTesterPods.xcworkspace \
  -scheme RNTester -testPlan RNTesterPointerTests \
  -configuration Debug -sdk iphonesimulator \
  -destination "platform=iOS Simulator,id=$SIMULATOR_UDID" \
  -derivedDataPath "$DERIVED_DATA" -resultBundlePath "$RESULT_BUNDLE" -jobs 3 \
  -only-testing:RNTesterPointerTests/RCTSurfacePointerHandlerTests/testLoneTouchAfterActiveResetIsPrimary \
  -parallel-testing-enabled NO \
  -test-timeouts-enabled YES -maximum-test-execution-time-allowance 120 \
  CODE_SIGNING_ALLOWED=NO

The first attempt added the regression to the default test plan, which compiled and linked the supplied fixture but failed while compiling unchanged RNTesterUnitTests/RCTImageLoaderTests.m at undefined RCTDefineImageURLLoader / RCTDefineImageDecoder invocations. Explicit test skipping still left that target in Xcode's build graph. The focused plan selects this regression without modifying that source or any existing assertion. A clean-base whole-suite verdict is not claimed. Xcode also rejected an initial -test-iterations 1 command before executing tests; the actual commands above omit that option. The earlier private-device-set/vendor-launcher block is resolved by this lane's standard Xcode execution on a default-set device.

The XCTest uses a fake UITouch and observes real handler dispatch before queueing; the separate Playground/HID experiment above supplies genuine input and JS delivery evidence.

Not checked: the full RNTester unit suite, empty reset or consecutive resets without a new active blue contact, ordinary external cancellation, more than two contacts, mouse/pen, physical devices, other widths/larger text/dark mode, natural production reset frequency, performance, or a fixed release. Android is outside this iOS-only reset correction. This is a local draft pending maintainer review and publication.

A separate iOS guest run passed ordinary input, clearing, three active-reset/recovery trials and leaving/returning. Each reset showed old primary down=1/cancel=1 and a new primary down=1/move=40/up=1. The reset control was absent from Components and reappeared in Playground. Fresh screenshot review approved the current light-mode portrait layout; other themes, larger text and other widths remain unreviewed.

MANDATORY Reproducer

https://github.com/alleneubank/react-native/commit/c420537cfe404bd7e087f22eb63925430acdd3a7 (failing RNTesterPlayground change on v0.88.1; the complete patch is also included above).

Screenshots and Videos

Captures are available on request; the runtime status is recorded above. No video is claimed.

Vorherrschende Sprache
C++
Sterne
127k
Forks
25.5k
Ø Merge
3 T. 13 Std.
Gemergte PRs (30 T.)
171

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus react/react-native

Alle Issues in react/react-native

Ähnliche Issues

Weitere Issues zu C++

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.