diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 9fc71c326..f6cf17da5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -633,6 +633,9 @@ DEBAAE552A42174A00BF2C1C /* OSInAppMessageView.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAAE512A42174A00BF2C1C /* OSInAppMessageView.h */; }; DEBAAE562A42174A00BF2C1C /* OSInAppMessageViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAAE522A42174A00BF2C1C /* OSInAppMessageViewController.h */; }; DEBAAE572A42174A00BF2C1C /* OSInAppMessageView.m in Sources */ = {isa = PBXBuildFile; fileRef = DEBAAE532A42174A00BF2C1C /* OSInAppMessageView.m */; }; + 5C1A7E0D2FB0A00100AB0004 /* OSInAppMessageInsets.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C1A7E0D2FB0A00100AB0001 /* OSInAppMessageInsets.h */; }; + 5C1A7E0D2FB0A00100AB0005 /* OSInAppMessageInsets.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A7E0D2FB0A00100AB0002 /* OSInAppMessageInsets.m */; }; + 5C1A7E0D2FB0A00100AB0006 /* OSInAppMessageInsetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A7E0D2FB0A00100AB0003 /* OSInAppMessageInsetsTests.swift */; }; DEBAAE602A42175A00BF2C1C /* OSDynamicTriggerController.m in Sources */ = {isa = PBXBuildFile; fileRef = DEBAAE582A42175900BF2C1C /* OSDynamicTriggerController.m */; }; DEBAAE612A42175A00BF2C1C /* OSMessagingController.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAAE592A42175900BF2C1C /* OSMessagingController.h */; }; DEBAAE622A42175A00BF2C1C /* OSInAppMessageController.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAAE5A2A42175900BF2C1C /* OSInAppMessageController.h */; }; @@ -1858,6 +1861,9 @@ DEBAAE512A42174A00BF2C1C /* OSInAppMessageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageView.h; sourceTree = ""; }; DEBAAE522A42174A00BF2C1C /* OSInAppMessageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageViewController.h; sourceTree = ""; }; DEBAAE532A42174A00BF2C1C /* OSInAppMessageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageView.m; sourceTree = ""; }; + 5C1A7E0D2FB0A00100AB0001 /* OSInAppMessageInsets.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageInsets.h; sourceTree = ""; }; + 5C1A7E0D2FB0A00100AB0002 /* OSInAppMessageInsets.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageInsets.m; sourceTree = ""; }; + 5C1A7E0D2FB0A00100AB0003 /* OSInAppMessageInsetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSInAppMessageInsetsTests.swift; sourceTree = ""; }; DEBAAE582A42175900BF2C1C /* OSDynamicTriggerController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSDynamicTriggerController.m; sourceTree = ""; }; DEBAAE592A42175900BF2C1C /* OSMessagingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSMessagingController.h; sourceTree = ""; }; DEBAAE5A2A42175900BF2C1C /* OSInAppMessageController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageController.h; sourceTree = ""; }; @@ -2384,6 +2390,7 @@ 3CB35FCA2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift */, 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */, 67ECA2928D863073B785F93F /* IamFetchIdentityVerificationTests.swift */, + 5C1A7E0D2FB0A00100AB0003 /* OSInAppMessageInsetsTests.swift */, ); path = OneSignalInAppMessagesTests; sourceTree = ""; @@ -3358,6 +3365,8 @@ DEBAAE532A42174A00BF2C1C /* OSInAppMessageView.m */, DEBAAE522A42174A00BF2C1C /* OSInAppMessageViewController.h */, DEBAAE502A42174A00BF2C1C /* OSInAppMessageViewController.m */, + 5C1A7E0D2FB0A00100AB0001 /* OSInAppMessageInsets.h */, + 5C1A7E0D2FB0A00100AB0002 /* OSInAppMessageInsets.m */, ); path = UI; sourceTree = ""; @@ -3689,6 +3698,7 @@ DEBAAE662A42175A00BF2C1C /* OSDynamicTriggerController.h in Headers */, DEBAAE642A42175A00BF2C1C /* OSTriggerController.h in Headers */, DEBAAE562A42174A00BF2C1C /* OSInAppMessageViewController.h in Headers */, + 5C1A7E0D2FB0A00100AB0004 /* OSInAppMessageInsets.h in Headers */, DEBAAEB82A4381AE00BF2C1C /* OSInAppMessageMigrationController.h in Headers */, DEBAAE7E2A42176800BF2C1C /* OSInAppMessageDisplayStats.h in Headers */, DEBAAE882A42176800BF2C1C /* OSInAppMessageClickEvent.h in Headers */, @@ -4654,6 +4664,7 @@ 3C01519C2C2E29F90079E076 /* IAMRequestTests.m in Sources */, 3CB35FCB2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift in Sources */, 6F894909CB6D09258DCCA1C9 /* IamFetchIdentityVerificationTests.swift in Sources */, + 5C1A7E0D2FB0A00100AB0006 /* OSInAppMessageInsetsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5142,6 +5153,7 @@ DEBAAE8C2A42176800BF2C1C /* OSInAppMessagePushPrompt.m in Sources */, DEBAAE8E2A42176800BF2C1C /* OSInAppMessageClickResult.m in Sources */, DEBAAE572A42174A00BF2C1C /* OSInAppMessageView.m in Sources */, + 5C1A7E0D2FB0A00100AB0005 /* OSInAppMessageInsets.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.h new file mode 100644 index 000000000..8a0514063 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.h @@ -0,0 +1,46 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OSInAppMessageInsets : NSObject + +// Each side is the larger of the margin and its safe area inset. Region frames are in the scene +// view's coordinates; one sitting inside a side safe area, not flush with the edge, adds clearance. +// Both checks allow 1pt of slack: within 1pt of the screen edge counts as flush, and up to 1pt past +// the safe area edge still counts as inside. +// Full screen messages ignore the safe area and regions. ++ (UIEdgeInsets)horizontalInsetsWithMargin:(CGFloat)margin + safeArea:(UIEdgeInsets)safeArea + width:(CGFloat)width + regionFrames:(NSArray *)regionFrames + isFullscreen:(BOOL)isFullscreen; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.m new file mode 100644 index 000000000..1088849c9 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageInsets.m @@ -0,0 +1,55 @@ +/** + * Modified MIT License + * + * Copyright 2026 OneSignal + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * 1. The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 2. All copies of substantial portions of the Software may only be used in connection + * with services provided by OneSignal. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#import "OSInAppMessageInsets.h" + +@implementation OSInAppMessageInsets + ++ (UIEdgeInsets)horizontalInsetsWithMargin:(CGFloat)margin + safeArea:(UIEdgeInsets)safeArea + width:(CGFloat)width + regionFrames:(NSArray *)regionFrames + isFullscreen:(BOOL)isFullscreen { + if (isFullscreen) { + return UIEdgeInsetsMake(0, margin, 0, margin); + } + CGFloat left = MAX(margin, safeArea.left); + CGFloat right = MAX(margin, safeArea.right); + // Region frames already include UIKit's interactive margins; the message margin is added on top. + for (NSValue *value in regionFrames) { + CGRect frame = value.CGRectValue; + CGFloat minX = CGRectGetMinX(frame); + CGFloat maxX = CGRectGetMaxX(frame); + if (minX >= 1 && maxX <= safeArea.left + 1) { + left = MAX(left, maxX + margin); + } else if (maxX <= width - 1 && minX >= width - safeArea.right - 1) { + right = MAX(right, width - minX + margin); + } + } + return UIEdgeInsetsMake(0, left, 0, right); +} + +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 1610b5a48..c4d7b805c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -27,6 +27,7 @@ #import "OSInAppMessageViewController.h" #import "OSInAppMessageView.h" +#import "OSInAppMessageInsets.h" #import "OSInAppMessageController.h" #import "OSInAppMessageBridgeEvent.h" #import @@ -440,34 +441,26 @@ - (UIView *)sceneView { return self.view; } -// Each side uses the larger of the margin and its safe area inset, and on iOS 27.1+ keeps the margin -// from hardware inside a side safe area (e.g. a foldable's camera), matching system notification banners. -// Regions flush with the edge, like a status bar column, are already covered by the safe area. -// Region frames already include UIKit's interactive margins; the message margin is added on top. +// On iOS 27.1+ the message also keeps clear of hardware inside a side safe area (e.g. a foldable's camera), +// matching system notification banners. - (UIEdgeInsets)horizontalInsets { UIView *sceneView = [self sceneView]; CGFloat margin = self.useWidthMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; - UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : sceneView.safeAreaInsets; - CGFloat left = MAX(margin, safeArea.left); - CGFloat right = MAX(margin, safeArea.right); + NSMutableArray *regionFrames = [NSMutableArray new]; // Full screen content clears hardware itself, using the insets passed through setSafeAreaInsets - if (self.isFullscreen) { - return UIEdgeInsetsMake(0, left, 0, right); - } - if (@available(iOS 27.1, *)) { - CGFloat width = sceneView.bounds.size.width; - id kind = [(id)NSClassFromString(@"UIViewReservedRegionKind") occlusionRegionKind]; - for (id region in [(id)sceneView reservedRegionsOfKind:kind]) { - CGFloat minX = CGRectGetMinX(region.frame); - CGFloat maxX = CGRectGetMaxX(region.frame); - if (minX >= 1 && maxX <= safeArea.left + 1) { - left = MAX(left, maxX + margin); - } else if (maxX <= width - 1 && minX >= width - safeArea.right - 1) { - right = MAX(right, width - minX + margin); + if (!self.isFullscreen) { + if (@available(iOS 27.1, *)) { + id kind = [(id)NSClassFromString(@"UIViewReservedRegionKind") occlusionRegionKind]; + for (id region in [(id)sceneView reservedRegionsOfKind:kind]) { + [regionFrames addObject:[NSValue valueWithCGRect:region.frame]]; } } } - return UIEdgeInsetsMake(0, left, 0, right); + return [OSInAppMessageInsets horizontalInsetsWithMargin:margin + safeArea:sceneView.safeAreaInsets + width:sceneView.bounds.size.width + regionFrames:regionFrames + isFullscreen:self.isFullscreen]; } - (void)updateHorizontalInsets { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSInAppMessageInsetsTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSInAppMessageInsetsTests.swift new file mode 100644 index 000000000..d3fc90df1 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSInAppMessageInsetsTests.swift @@ -0,0 +1,131 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest + +final class OSInAppMessageInsetsTests: XCTestCase { + private let width: CGFloat = 800 + private let margin: CGFloat = 24 + // Landscape with a side safe area on each side, e.g. a camera housing on the left. + private let safeArea = UIEdgeInsets(top: 0, left: 59, bottom: 21, right: 47) + + private func insets(margin: CGFloat? = nil, + safeArea: UIEdgeInsets? = nil, + regions: [CGRect] = [], + isFullscreen: Bool = false) -> UIEdgeInsets { + OSInAppMessageInsets.horizontalInsets(withMargin: margin ?? self.margin, + safeArea: safeArea ?? self.safeArea, + width: width, + regionFrames: regions.map { NSValue(cgRect: $0) }, + isFullscreen: isFullscreen) + } + + func testNoRegions_usesSafeAreaWhenLargerThanMargin() { + XCTAssertEqual(insets(), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 47)) + } + + func testMarginAndSafeArea_takesMaxNotSum() { + let result = insets(safeArea: UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 30)) + XCTAssertEqual(result, UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 30)) + } + + func testNoSafeArea_usesMargin() { + XCTAssertEqual(insets(safeArea: .zero), UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24)) + } + + func testNoWidthMargin_usesSafeAreaOnly() { + let result = insets(margin: 0, safeArea: UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) + XCTAssertEqual(result, UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) + } + + func testFullscreen_ignoresSafeAreaAndRegions() { + let region = CGRect(x: 10, y: 100, width: 40, height: 80) + XCTAssertEqual(insets(regions: [region], isFullscreen: true), + UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24)) + XCTAssertEqual(insets(margin: 0, regions: [region], isFullscreen: true), .zero) + } + + func testRegionInsideLeftSafeArea_addsClearance() { + let region = CGRect(x: 10, y: 100, width: 40, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 74, bottom: 0, right: 47)) + } + + func testRegionInsideRightSafeArea_addsClearance() { + let region = CGRect(x: 760, y: 100, width: 30, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 64)) + } + + func testRegionsOnBothSides_addClearanceToEach() { + let left = CGRect(x: 10, y: 100, width: 40, height: 80) + let right = CGRect(x: 760, y: 100, width: 30, height: 80) + XCTAssertEqual(insets(regions: [left, right]), UIEdgeInsets(top: 0, left: 74, bottom: 0, right: 64)) + } + + func testRegionClearanceSmallerThanSafeArea_keepsSafeArea() { + let region = CGRect(x: 2, y: 100, width: 20, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 47)) + } + + func testRegionFlushWithEdge_isIgnored() { + let left = CGRect(x: 0, y: 0, width: 59, height: 400) + let right = CGRect(x: 753, y: 0, width: 47, height: 400) + XCTAssertEqual(insets(regions: [left, right]), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 47)) + } + + func testRegionStraddlingSafeAreaEdge_isIgnored() { + let left = CGRect(x: 40, y: 100, width: 40, height: 80) + let right = CGRect(x: 740, y: 100, width: 40, height: 80) + XCTAssertEqual(insets(regions: [left, right]), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 47)) + } + + func testRegionWithinOnePointOfSafeAreaEdge_addsClearance() { + let region = CGRect(x: 10, y: 100, width: 50, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 84, bottom: 0, right: 47)) + } + + func testRightRegionWithinOnePointOfSafeAreaEdge_addsClearance() { + let region = CGRect(x: 752, y: 100, width: 30, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 72)) + } + + func testRegionStartingAtOnePoint_addsClearance() { + let region = CGRect(x: 1, y: 100, width: 40, height: 80) + XCTAssertEqual(insets(regions: [region]), UIEdgeInsets(top: 0, left: 65, bottom: 0, right: 47)) + } + + func testTwoRegionsOnLeftSide_keepLargerClearance() { + let regions = [CGRect(x: 10, y: 100, width: 40, height: 80), CGRect(x: 10, y: 200, width: 30, height: 80)] + XCTAssertEqual(insets(regions: regions), UIEdgeInsets(top: 0, left: 74, bottom: 0, right: 47)) + XCTAssertEqual(insets(regions: regions.reversed()), UIEdgeInsets(top: 0, left: 74, bottom: 0, right: 47)) + } + + func testTwoRegionsOnRightSide_keepLargerClearance() { + let regions = [CGRect(x: 760, y: 100, width: 30, height: 80), CGRect(x: 790, y: 100, width: 5, height: 80)] + XCTAssertEqual(insets(regions: regions), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 64)) + XCTAssertEqual(insets(regions: regions.reversed()), UIEdgeInsets(top: 0, left: 59, bottom: 0, right: 64)) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h index 53adad582..e81deeaab 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h @@ -6,6 +6,7 @@ #import "OSInAppMessageController.h" #import "OSMessagingController.h" #import "OSInAppMessagingRequests.h" +#import "OSInAppMessageInsets.h" // Expose private properties and methods for testing @interface OSMessagingController (Testing)