diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 0daf95a05ec..3b12b66c4d3 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -2528,6 +2528,10 @@ FAF0F3CA2EA7DD0700E44E9B /* SentryANRTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D45C039F2EDF0E4700975137 /* Public */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Public; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 63AA75971EB8AEF500D153DE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -4465,6 +4469,7 @@ 621D9F2D2B9B030E003D94DE /* Helper */, D44DB3802DDCCF1700174EF4 /* Persistence */, D8F016B12B9622B7007B9AFB /* Protocol */, + D45C039F2EDF0E4700975137 /* Public */, D856272A2A374A6800FB8062 /* Tools */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, FA4C32962DF7513F001D7B00 /* SentryExperimentalOptions.swift */, @@ -5410,6 +5415,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D45C039F2EDF0E4700975137 /* Public */, + ); name = Sentry; productName = "Sentry-iOS"; productReference = 63AA759B1EB8AEF500D153DE /* Sentry.framework */; diff --git a/Sources/Sentry/SentrySDKInternal.m b/Sources/Sentry/SentrySDKInternal.m index 9ae1320aa69..75dda021d60 100644 --- a/Sources/Sentry/SentrySDKInternal.m +++ b/Sources/Sentry/SentrySDKInternal.m @@ -109,6 +109,14 @@ + (SentryReplayApi *)replay dispatch_once(&onceToken, ^{ replay = [[SentryReplayApi alloc] init]; }); return replay; } + ++ (SentryScreenshotApi *)screenshot +{ + static SentryScreenshotApi *screenshot; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ screenshot = [[SentryScreenshotApi alloc] init]; }); + return screenshot; +} #endif /** Internal, only needed for testing. */ diff --git a/Sources/Sentry/SentryScreenshotIntegration.m b/Sources/Sentry/SentryScreenshotIntegration.m index ddbe3d65149..d9f18eceb2a 100644 --- a/Sources/Sentry/SentryScreenshotIntegration.m +++ b/Sources/Sentry/SentryScreenshotIntegration.m @@ -7,6 +7,7 @@ # import "SentryEvent+Private.h" # import "SentryException.h" # import "SentryHub+Private.h" +# import "SentryInternalDefines.h" # import "SentrySDK+Private.h" # import "SentrySwift.h" @@ -26,6 +27,7 @@ @interface SentryScreenshotIntegration () @property (nonatomic, strong) SentryOptions *options; +@property (nonatomic, strong, nullable) SentryMaskingPreviewView *previewView; @end @@ -106,6 +108,41 @@ - (void)uninstall return result; } +- (void)showMaskPreview:(CGFloat)opacity +{ + SENTRY_LOG_DEBUG(@"[Screenshot] Showing mask preview with opacity: %f", opacity); + if ([SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced] == NO) { + SENTRY_LOG_DEBUG(@"[Screenshot] No tracing is active, not showing mask preview"); + return; + } + + UIWindow *window = + [SentryDependencyContainer.sharedInstance.application getWindows].firstObject; + if (window == nil) { + SENTRY_LOG_WARN(@"[Screenshot] No UIWindow available to display preview"); + return; + } + + if (_previewView == nil) { + SentryViewScreenshotOptions *screenshotOptions = self.options.screenshot; + _previewView = [[SentryMaskingPreviewView alloc] initWithRedactOptions:screenshotOptions]; + } + + SentryMaskingPreviewView *previewView = _previewView; + if (previewView != nil) { + previewView.opacity = opacity; + [previewView setFrame:window.bounds]; + [window addSubview:previewView]; + } +} + +- (void)hideMaskPreview +{ + SENTRY_LOG_DEBUG(@"[Screenshot] Hiding mask preview"); + [_previewView removeFromSuperview]; + _previewView = nil; +} + @end #endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySDKInternal.h b/Sources/Sentry/include/SentrySDKInternal.h index 5200341a3a7..6626a800a61 100644 --- a/Sources/Sentry/include/SentrySDKInternal.h +++ b/Sources/Sentry/include/SentrySDKInternal.h @@ -47,6 +47,11 @@ SENTRY_NO_INIT * API to control session replay */ @property (class, nonatomic, readonly) SentryReplayApi *replay; + +/** + * API to control screenshot masking + */ +@property (class, nonatomic, readonly) NSObject *screenshot; #endif /** diff --git a/Sources/Sentry/include/SentryScreenshotIntegration.h b/Sources/Sentry/include/SentryScreenshotIntegration.h index 735f1bfa1ab..1f0a6ac51b7 100644 --- a/Sources/Sentry/include/SentryScreenshotIntegration.h +++ b/Sources/Sentry/include/SentryScreenshotIntegration.h @@ -9,6 +9,33 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryScreenshotIntegration : SentryBaseIntegration +/** + * Shows an overlay on the app to debug screenshot masking. + * + * By calling this function an overlay will appear covering the parts + * of the app that will be masked for screenshots. + * This will only work if the debugger is attached and it will + * cause some slow frames. + * + * @param opacity The opacity of the overlay. + * + * @note This method must be called from the main thread. + * + * @warning This is an experimental feature and may still have bugs. + * Do not use this in production. + */ +- (void)showMaskPreview:(CGFloat)opacity; + +/** + * Removes the overlay that shows screenshot masking. + * + * @note This method must be called from the main thread. + * + * @warning This is an experimental feature and may still have bugs. + * Do not use this in production. + */ +- (void)hideMaskPreview; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Helper/SentrySDK.swift b/Sources/Swift/Helper/SentrySDK.swift index e2a3df3c402..fd911a81ca5 100644 --- a/Sources/Swift/Helper/SentrySDK.swift +++ b/Sources/Swift/Helper/SentrySDK.swift @@ -23,6 +23,13 @@ import Foundation @objc public static var replay: SentryReplayApi { return SentrySDKInternal.replay } + + /// API to control screenshot masking + @objc public static var screenshot: SentryScreenshotApi { + // swiftlint:disable force_cast + return SentrySDKInternal.screenshot as! SentryScreenshotApi + // swiftlint:enable force_cast + } #endif /// API to access Sentry logs diff --git a/Sources/Swift/Public/SentryScreenshotApi.swift b/Sources/Swift/Public/SentryScreenshotApi.swift new file mode 100644 index 00000000000..d72fc80f526 --- /dev/null +++ b/Sources/Swift/Public/SentryScreenshotApi.swift @@ -0,0 +1,115 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +@_implementationOnly import _SentryPrivate +import Foundation +import UIKit + +/** + * API to control screenshot masking. + */ +@objcMembers +public final class SentryScreenshotApi: NSObject { + + /** + * Marks this view to be masked during screenshots. + */ + @objc(maskView:) + public func maskView(_ view: UIView) { + SentryRedactViewHelper.maskView(view) + } + + /** + * Marks this view to not be masked during screenshot masking. + */ + @objc(unmaskView:) + public func unmaskView(_ view: UIView) { + SentryRedactViewHelper.unmaskView(view) + } + + /** + * Shows an overlay on the app to debug screenshot masking. + * + * By calling this function an overlay will appear covering the parts + * of the app that will be masked for screenshots. + * This will only work if the debugger is attached and it will + * cause some slow frames. + * + * - note: This method must be called from the main thread. + * + * - warning: This is an experimental feature and may still have bugs. + * Do not use this in production. + */ + @objc(showMaskPreview) + public func showMaskPreview() { + showMaskPreview(opacity: 1.0) + } + + /** + * Shows an overlay on the app to debug screenshot masking. + * + * By calling this function an overlay will appear covering the parts + * of the app that will be masked for screenshots. + * This will only work if the debugger is attached and it will + * cause some slow frames. + * + * - parameter opacity: The opacity of the overlay. + * + * - note: This method must be called from the main thread. + * + * - warning: This is an experimental feature and may still have bugs. + * Do not use this in production. + */ + @objc(showMaskPreviewWithOpacity:) + public func showMaskPreview(opacity: CGFloat) { + SentrySDKLog.debug("[Screenshot] Showing mask preview with opacity: \(opacity)") + // Use Objective-C runtime to get the integration class since it's not directly accessible from Swift + guard let integrationClass = NSClassFromString("SentryScreenshotIntegration") as? NSObject.Type else { + SentrySDKLog.debug("[Screenshot] Screenshot integration class not found") + return + } + guard let screenshotIntegration = SentrySDKInternal.currentHub() + .getInstalledIntegration(integrationClass) else { + SentrySDKLog.debug("[Screenshot] Screenshot integration not installed") + return + } + + // Use performSelector to call the Objective-C method + let selector = NSSelectorFromString("showMaskPreview:") + if screenshotIntegration.responds(to: selector) { + screenshotIntegration.perform(selector, with: opacity) + } + } + + /** + * Removes the overlay that shows screenshot masking. + * + * - note: This method must be called from the main thread. + * + * - warning: This is an experimental feature and may still have bugs. + * Do not use this in production. + */ + @objc(hideMaskPreview) + public func hideMaskPreview() { + SentrySDKLog.debug("[Screenshot] Hiding mask preview") + // Use Objective-C runtime to get the integration class since it's not directly accessible from Swift + guard let integrationClass = NSClassFromString("SentryScreenshotIntegration") as? NSObject.Type else { + SentrySDKLog.debug("[Screenshot] Screenshot integration class not found") + return + } + guard let screenshotIntegration = SentrySDKInternal.currentHub() + .getInstalledIntegration(integrationClass) else { + SentrySDKLog.debug("[Screenshot] Screenshot integration not installed") + return + } + + // Use performSelector to call the Objective-C method + let selector = NSSelectorFromString("hideMaskPreview") + if screenshotIntegration.responds(to: selector) { + screenshotIntegration.perform(selector) + } + } +} + +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT