143 lines
6.0 KiB
Diff
143 lines
6.0 KiB
Diff
diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
|
|
index 76bb694854b29d7f779f38244cd48113b429ea1f..fd402ac938862e8d39c5467c1b5c86c4e7eb0c83 100644
|
|
--- a/android/src/main/java/com/swmansion/rnscreens/Screen.kt
|
|
+++ b/android/src/main/java/com/swmansion/rnscreens/Screen.kt
|
|
@@ -16,6 +16,7 @@ import androidx.annotation.RequiresApi
|
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
import androidx.core.view.children
|
|
import androidx.fragment.app.Fragment
|
|
+import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
import com.facebook.react.bridge.ReactContext
|
|
import com.facebook.react.uimanager.PixelUtil
|
|
@@ -462,7 +463,7 @@ class Screen(
|
|
endTransitionRecursive(childView.toolbar)
|
|
}
|
|
|
|
- if (childView is ViewGroup) {
|
|
+ if (childView is ViewGroup && childView !is RecyclerView) {
|
|
endTransitionRecursive(childView)
|
|
}
|
|
}
|
|
@@ -491,7 +492,10 @@ class Screen(
|
|
startTransitionRecursive(child.toolbar)
|
|
}
|
|
|
|
- if (child is ViewGroup) {
|
|
+ // Transition a RecyclerView as one unit. Marking its recyclable children as
|
|
+ // transitioning keeps their parent set after removal, so RecyclerView crashes
|
|
+ // when it tries to recycle them during the screen transition.
|
|
+ if (child is ViewGroup && child !is RecyclerView) {
|
|
startTransitionRecursive(child)
|
|
}
|
|
}
|
|
diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm
|
|
index 9b66012dc8db1e3af0cb5457f8dad86e7f2fe5e3..a7f1f613c291de8421f6f117c06d35ba34a2d908 100644
|
|
--- a/ios/RNSScreen.mm
|
|
+++ b/ios/RNSScreen.mm
|
|
@@ -1,4 +1,5 @@
|
|
#import <UIKit/UIKit.h>
|
|
+#import <objc/runtime.h>
|
|
|
|
#import "RNSModalScreen.h"
|
|
#import "RNSScreen.h"
|
|
@@ -1180,9 +1181,82 @@ - (BOOL)hasHeaderConfig
|
|
return _config != nil;
|
|
}
|
|
|
|
+/*
|
|
+ * PATCH (secure-snapshot): content protected by an isSecureTextEntry canvas
|
|
+ * (per-view capture protection, e.g. expo-privacy-sensitive) is stripped from
|
|
+ * the render-server snapshot RNSScreen takes for JS-driven pops, so the user
|
|
+ * sees the hidden fallback (the GrowthHack butterfly) during the transition.
|
|
+ * The capture-exclusion flag is only consulted for layers INSIDE a captured
|
|
+ * subtree, so a snapshot rooted BELOW the flagged canvas still includes the
|
|
+ * content.
|
|
+ *
|
|
+ * By the time setViewToSnapshot runs, the screen's children are already
|
|
+ * unmounted (only the pixels survive on the render server), so the secure
|
|
+ * subtrees must be captured HERE - before the transaction's mutations apply,
|
|
+ * while the subtree is intact. The overlays are stashed on the screen view
|
|
+ * and composited over the main snapshot in setViewToSnapshot, each re-wrapped
|
|
+ * in a fresh secure canvas so real captures (screenshots/recordings) taken
|
|
+ * during the transition still exclude them.
|
|
+ */
|
|
+static void RNSCollectPrivacySensitiveViews(UIView *view, NSMutableArray<UIView *> *out)
|
|
+{
|
|
+ // Swift classes are namespaced in the ObjC runtime ("Module.ClassName")
|
|
+ if ([NSStringFromClass([view class]) hasSuffix:@"ExpoPrivacySensitiveView"]) {
|
|
+ [out addObject:view];
|
|
+ return;
|
|
+ }
|
|
+ for (UIView *subview in view.subviews) {
|
|
+ RNSCollectPrivacySensitiveViews(subview, out);
|
|
+ }
|
|
+}
|
|
+
|
|
+static UIView *_Nullable RNSMakeSecureCanvas(void)
|
|
+{
|
|
+ UITextField *field = [UITextField new];
|
|
+ field.secureTextEntry = YES;
|
|
+ field.userInteractionEnabled = NO;
|
|
+ UIView *canvas = (UIView *)field.layer.sublayers.firstObject.delegate;
|
|
+ if (![canvas isKindOfClass:[UIView class]]) {
|
|
+ return nil;
|
|
+ }
|
|
+ canvas.clipsToBounds = NO;
|
|
+ canvas.backgroundColor = UIColor.clearColor;
|
|
+ // retain the field alongside the canvas in case the capture exclusion ever
|
|
+ // depends on the owning field staying alive
|
|
+ objc_setAssociatedObject(canvas, "RNSSecureCanvasField", field, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
+ return canvas;
|
|
+}
|
|
+
|
|
+static char RNSSecureOverlaysKey;
|
|
+
|
|
- (void)willBeUnmountedInUpcomingTransaction
|
|
{
|
|
_markedForUnmountInCurrentTransaction = YES;
|
|
+
|
|
+ NSMutableArray<UIView *> *sensitiveViews = [NSMutableArray new];
|
|
+ RNSCollectPrivacySensitiveViews(self, sensitiveViews);
|
|
+ NSMutableArray<UIView *> *overlays = [NSMutableArray new];
|
|
+ for (UIView *sensitiveView in sensitiveViews) {
|
|
+ UIView *canvas = sensitiveView.subviews.firstObject;
|
|
+ for (UIView *content in canvas.subviews) {
|
|
+ UIView *pieceSnapshot = [content snapshotViewAfterScreenUpdates:NO];
|
|
+ if (pieceSnapshot == nil) {
|
|
+ continue;
|
|
+ }
|
|
+ CGRect frameInScreen = [content convertRect:content.bounds toView:self];
|
|
+ UIView *secureWrap = RNSMakeSecureCanvas();
|
|
+ if (secureWrap != nil) {
|
|
+ secureWrap.frame = frameInScreen;
|
|
+ pieceSnapshot.frame = secureWrap.bounds;
|
|
+ [secureWrap addSubview:pieceSnapshot];
|
|
+ [overlays addObject:secureWrap];
|
|
+ } else {
|
|
+ pieceSnapshot.frame = frameInScreen;
|
|
+ [overlays addObject:pieceSnapshot];
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ objc_setAssociatedObject(self, &RNSSecureOverlaysKey, overlays.count > 0 ? overlays : nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|
|
|
|
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -1999,6 +2073,15 @@ - (void)setViewToSnapshot
|
|
auto afterUpdates = self.screenView.snapshotAfterUpdates;
|
|
UIView *snapshot = [self.view snapshotViewAfterScreenUpdates:afterUpdates];
|
|
snapshot.frame = self.view.frame;
|
|
+
|
|
+ // PATCH (secure-snapshot): composite the secure-content overlays captured
|
|
+ // in willBeUnmountedInUpcomingTransaction (see comment there)
|
|
+ NSArray<UIView *> *overlays = objc_getAssociatedObject(self.screenView, &RNSSecureOverlaysKey);
|
|
+ for (UIView *overlay in overlays) {
|
|
+ [snapshot addSubview:overlay];
|
|
+ }
|
|
+ objc_setAssociatedObject(self.screenView, &RNSSecureOverlaysKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
+
|
|
[self.view removeFromSuperview];
|
|
self.view = snapshot;
|
|
[superView addSubview:snapshot];
|