@klarna/react-native-klarna-network-messaging 0.0.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -28
- package/android/build.gradle +97 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementErrorEvent.kt +25 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementResizedEvent.kt +20 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementViewManager.kt +251 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingViewManagerHelpers.kt +52 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaNetworkMessagingPackage.kt +19 -0
- package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/RNKlarnaMessagingPlacementViewSpec.kt +22 -0
- package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementEventsTest.kt +39 -0
- package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementViewManagerTest.kt +76 -0
- package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingViewManagerHelpersTest.kt +200 -0
- package/ios/Sources/KlarnaMessagingPlacementViewFactory.swift +101 -0
- package/ios/Sources/KlarnaMessagingPlacementViewHelper.swift +171 -0
- package/ios/Sources/KlarnaMessagingPlacementViewManager.h +5 -0
- package/ios/Sources/KlarnaMessagingPlacementViewManager.mm +37 -0
- package/ios/Sources/view/KlarnaMessagingPlacementViewWrapper.h +7 -0
- package/ios/Sources/view/KlarnaMessagingPlacementViewWrapper.mm +183 -0
- package/ios/Tests/KlarnaMessagingHeightObserverTests.swift +204 -0
- package/ios/Tests/KlarnaMessagingPlacementViewFactoryTests.swift +167 -0
- package/ios/Tests/KlarnaMessagingPlacementViewHelperTests.swift +118 -0
- package/lib/commonjs/KlarnaMessagingPlacementView.js +102 -0
- package/lib/commonjs/KlarnaMessagingPlacementView.js.map +1 -0
- package/lib/commonjs/Specs/KlarnaMessagingPlacementViewNativeComponent.js +10 -0
- package/lib/commonjs/Specs/KlarnaMessagingPlacementViewNativeComponent.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/internal.js +45 -0
- package/lib/commonjs/internal.js.map +1 -0
- package/lib/commonjs/types/KlarnaMessagingError.js +2 -0
- package/lib/commonjs/types/KlarnaMessagingError.js.map +1 -0
- package/lib/commonjs/types/KlarnaMessagingPlacementConfiguration.js +6 -0
- package/lib/commonjs/types/KlarnaMessagingPlacementConfiguration.js.map +1 -0
- package/lib/commonjs/types/KlarnaMessagingPlacementType.js +15 -0
- package/lib/commonjs/types/KlarnaMessagingPlacementType.js.map +1 -0
- package/lib/module/KlarnaMessagingPlacementView.js +94 -0
- package/lib/module/KlarnaMessagingPlacementView.js.map +1 -0
- package/lib/module/Specs/KlarnaMessagingPlacementViewNativeComponent.js +3 -0
- package/lib/module/Specs/KlarnaMessagingPlacementViewNativeComponent.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/internal.js +38 -0
- package/lib/module/internal.js.map +1 -0
- package/lib/module/types/KlarnaMessagingError.js +2 -0
- package/lib/module/types/KlarnaMessagingError.js.map +1 -0
- package/lib/module/types/KlarnaMessagingPlacementConfiguration.js +2 -0
- package/lib/module/types/KlarnaMessagingPlacementConfiguration.js.map +1 -0
- package/lib/module/types/KlarnaMessagingPlacementType.js +9 -0
- package/lib/module/types/KlarnaMessagingPlacementType.js.map +1 -0
- package/lib/typescript/KlarnaMessagingPlacementView.d.ts +39 -0
- package/lib/typescript/KlarnaMessagingPlacementView.d.ts.map +1 -0
- package/lib/typescript/Specs/KlarnaMessagingPlacementViewNativeComponent.d.ts +23 -0
- package/lib/typescript/Specs/KlarnaMessagingPlacementViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/internal.d.ts +25 -0
- package/lib/typescript/internal.d.ts.map +1 -0
- package/lib/typescript/types/KlarnaMessagingError.d.ts +8 -0
- package/lib/typescript/types/KlarnaMessagingError.d.ts.map +1 -0
- package/lib/typescript/types/KlarnaMessagingPlacementConfiguration.d.ts +25 -0
- package/lib/typescript/types/KlarnaMessagingPlacementConfiguration.d.ts.map +1 -0
- package/lib/typescript/types/KlarnaMessagingPlacementType.d.ts +8 -0
- package/lib/typescript/types/KlarnaMessagingPlacementType.d.ts.map +1 -0
- package/package.json +122 -6
- package/react-native-klarna-network-messaging.podspec +28 -0
- package/src/KlarnaMessagingPlacementView.tsx +139 -0
- package/src/Specs/KlarnaMessagingPlacementViewNativeComponent.ts +32 -0
- package/src/index.tsx +12 -0
- package/src/internal.ts +48 -0
- package/src/types/KlarnaMessagingError.ts +7 -0
- package/src/types/KlarnaMessagingPlacementConfiguration.ts +29 -0
- package/src/types/KlarnaMessagingPlacementType.ts +7 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#import <react/renderer/components/KlarnaNetworkMessagingSpec/ComponentDescriptors.h>
|
|
2
|
+
#import <react/renderer/components/KlarnaNetworkMessagingSpec/EventEmitters.h>
|
|
3
|
+
#import <react/renderer/components/KlarnaNetworkMessagingSpec/Props.h>
|
|
4
|
+
#import <react/renderer/components/KlarnaNetworkMessagingSpec/RCTComponentViewHelpers.h>
|
|
5
|
+
#import "KlarnaMessagingPlacementViewWrapper.h"
|
|
6
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
7
|
+
|
|
8
|
+
// Framework form resolves under use_frameworks!; quoted form is the headermap fallback for static builds.
|
|
9
|
+
#if __has_include(<react_native_klarna_network_messaging/react_native_klarna_network_messaging-Swift.h>)
|
|
10
|
+
#import <react_native_klarna_network_messaging/react_native_klarna_network_messaging-Swift.h>
|
|
11
|
+
#else
|
|
12
|
+
#import "react_native_klarna_network_messaging-Swift.h"
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
using namespace facebook::react;
|
|
16
|
+
|
|
17
|
+
@interface KlarnaMessagingPlacementViewWrapper () <RCTRNKlarnaMessagingPlacementViewViewProtocol, KlarnaMessagingHeightDelegate>
|
|
18
|
+
|
|
19
|
+
@property (nonatomic, strong) UIView* placementView;
|
|
20
|
+
@property (nonatomic, strong) KlarnaMessagingHeightObserver* heightObserver;
|
|
21
|
+
|
|
22
|
+
@end
|
|
23
|
+
|
|
24
|
+
@implementation KlarnaMessagingPlacementViewWrapper
|
|
25
|
+
|
|
26
|
+
#pragma mark - Initialization
|
|
27
|
+
|
|
28
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
29
|
+
{
|
|
30
|
+
self = [super initWithFrame:frame];
|
|
31
|
+
if (self) {
|
|
32
|
+
self.backgroundColor = [UIColor clearColor];
|
|
33
|
+
self.clipsToBounds = NO;
|
|
34
|
+
self.heightObserver = [[KlarnaMessagingHeightObserver alloc] initWithDelayIntervals:@[@0.5, @1.5, @3.0]];
|
|
35
|
+
self.heightObserver.delegate = self;
|
|
36
|
+
static const auto defaultProps = std::make_shared<const RNKlarnaMessagingPlacementViewProps>();
|
|
37
|
+
_props = defaultProps;
|
|
38
|
+
}
|
|
39
|
+
return self;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
- (void)dealloc {
|
|
43
|
+
[self.heightObserver stopObserving];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
47
|
+
{
|
|
48
|
+
return concreteComponentDescriptorProvider<RNKlarnaMessagingPlacementViewComponentDescriptor>();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Class<RCTComponentViewProtocol> RNKlarnaMessagingPlacementViewCls(void)
|
|
52
|
+
{
|
|
53
|
+
return KlarnaMessagingPlacementViewWrapper.class;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#pragma mark - Props Update
|
|
57
|
+
|
|
58
|
+
/// Diffs all placement-related props against their previous values and triggers
|
|
59
|
+
/// a full view recreation when any of them change. Props are compared before
|
|
60
|
+
/// `[super updateProps:…]` stores the new snapshot.
|
|
61
|
+
- (void)updateProps:(const facebook::react::Props::Shared &)props oldProps:(const facebook::react::Props::Shared &)oldProps {
|
|
62
|
+
const auto &oldViewProps = *std::static_pointer_cast<RNKlarnaMessagingPlacementViewProps const>(_props);
|
|
63
|
+
const auto &newViewProps = *std::static_pointer_cast<RNKlarnaMessagingPlacementViewProps const>(props);
|
|
64
|
+
|
|
65
|
+
BOOL needsRecreate = oldViewProps.instanceId != newViewProps.instanceId
|
|
66
|
+
|| oldViewProps.placementType != newViewProps.placementType
|
|
67
|
+
|| oldViewProps.theme != newViewProps.theme
|
|
68
|
+
|| oldViewProps.amount != newViewProps.amount
|
|
69
|
+
|| oldViewProps.currency != newViewProps.currency;
|
|
70
|
+
|
|
71
|
+
[super updateProps:props oldProps:oldProps];
|
|
72
|
+
|
|
73
|
+
if (needsRecreate) {
|
|
74
|
+
NSString *instanceId = [[NSString alloc] initWithUTF8String:newViewProps.instanceId.c_str()];
|
|
75
|
+
NSString *placementType = [[NSString alloc] initWithUTF8String:newViewProps.placementType.c_str()];
|
|
76
|
+
NSString *theme = [[NSString alloc] initWithUTF8String:newViewProps.theme.c_str()];
|
|
77
|
+
NSString *amount = [[NSString alloc] initWithUTF8String:newViewProps.amount.c_str()];
|
|
78
|
+
NSString *currency = [[NSString alloc] initWithUTF8String:newViewProps.currency.c_str()];
|
|
79
|
+
|
|
80
|
+
[self recreatePlacementViewWithInstanceId:instanceId
|
|
81
|
+
placementType:placementType
|
|
82
|
+
theme:theme
|
|
83
|
+
amount:amount
|
|
84
|
+
currency:currency];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#pragma mark - Placement View Management
|
|
89
|
+
|
|
90
|
+
/// Tears down any existing SDK placement view and builds a fresh one from the
|
|
91
|
+
/// latest props. Validates required fields (instanceId, currency, amount), resolves
|
|
92
|
+
/// the Klarna instance from the core module, then creates and attaches the new
|
|
93
|
+
/// view with an active height observer.
|
|
94
|
+
- (void)recreatePlacementViewWithInstanceId:(NSString *)instanceId
|
|
95
|
+
placementType:(NSString *)placementType
|
|
96
|
+
theme:(NSString *)theme
|
|
97
|
+
amount:(NSString *)amount
|
|
98
|
+
currency:(NSString *)currency {
|
|
99
|
+
if (!instanceId || instanceId.length == 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!currency || currency.length == 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
NSNumber *parsedAmountNumber = [KlarnaMessagingPlacementViewHelper parseAmount:amount];
|
|
107
|
+
if (!parsedAmountNumber) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[self.heightObserver stopObserving];
|
|
112
|
+
[self.placementView removeFromSuperview];
|
|
113
|
+
self.placementView = nil;
|
|
114
|
+
|
|
115
|
+
id klarnaInstance = [KlarnaMessagingPlacementViewFactory resolveKlarnaWithInstanceId:instanceId];
|
|
116
|
+
if (!klarnaInstance) {
|
|
117
|
+
[self emitErrorWithName:@"NotInitialized"
|
|
118
|
+
message:@"No Klarna instance found for instanceId. Call Klarna.initialize() first."];
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
long long parsedAmount = parsedAmountNumber.longLongValue;
|
|
123
|
+
UIView *view = [KlarnaMessagingPlacementViewFactory
|
|
124
|
+
createPlacementViewWithKlarna:klarnaInstance
|
|
125
|
+
placementType:placementType ?: KlarnaMessagingPlacementViewFactory.defaultPlacementType
|
|
126
|
+
theme:theme ?: @""
|
|
127
|
+
amount:parsedAmount
|
|
128
|
+
currency:currency];
|
|
129
|
+
|
|
130
|
+
if (!view) {
|
|
131
|
+
[self emitErrorWithName:@"CreateFailed"
|
|
132
|
+
message:@"Failed to create KlarnaMessagingPlacementView."];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
self.placementView = view;
|
|
137
|
+
[KlarnaMessagingPlacementViewHelper addPlacementSubview:view to:self pinWidth:NO];
|
|
138
|
+
[self.heightObserver observe:view];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#pragma mark - Event Emission
|
|
142
|
+
|
|
143
|
+
/// Emits an `onError` Fabric event carrying a `{name, message}` error payload.
|
|
144
|
+
- (void)emitErrorWithName:(NSString *)name message:(NSString *)message {
|
|
145
|
+
if (_eventEmitter) {
|
|
146
|
+
auto emitter = std::dynamic_pointer_cast<const RNKlarnaMessagingPlacementViewEventEmitter>(_eventEmitter);
|
|
147
|
+
if (emitter) {
|
|
148
|
+
emitter->onError(RNKlarnaMessagingPlacementViewEventEmitter::OnError{
|
|
149
|
+
.error = {
|
|
150
|
+
.name = std::string([name UTF8String]),
|
|
151
|
+
.message = std::string([message UTF8String]),
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#pragma mark - KlarnaMessagingHeightDelegate
|
|
159
|
+
|
|
160
|
+
/// Called by `KlarnaMessagingHeightObserver` when the placement content height
|
|
161
|
+
/// changes. Emits an `onResized` Fabric event so the JS side can adjust layout.
|
|
162
|
+
- (void)placementViewDidUpdateHeight:(CGFloat)height {
|
|
163
|
+
if (_eventEmitter) {
|
|
164
|
+
auto emitter = std::dynamic_pointer_cast<const RNKlarnaMessagingPlacementViewEventEmitter>(_eventEmitter);
|
|
165
|
+
if (emitter) {
|
|
166
|
+
emitter->onResized(RNKlarnaMessagingPlacementViewEventEmitter::OnResized{
|
|
167
|
+
.height = std::string([[[NSNumber numberWithFloat:height] stringValue] UTF8String]),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#pragma mark - Layout
|
|
174
|
+
|
|
175
|
+
- (void)layoutSubviews {
|
|
176
|
+
[super layoutSubviews];
|
|
177
|
+
if (self.placementView) {
|
|
178
|
+
[KlarnaMessagingPlacementViewFactory clearBackgrounds:self.placementView];
|
|
179
|
+
}
|
|
180
|
+
[self.heightObserver checkAndReportHeight];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import react_native_klarna_network_messaging
|
|
3
|
+
|
|
4
|
+
class MockHeightDelegate: NSObject, KlarnaMessagingHeightDelegate {
|
|
5
|
+
var reportedHeights: [CGFloat] = []
|
|
6
|
+
|
|
7
|
+
func placementViewDidUpdateHeight(_ height: CGFloat) {
|
|
8
|
+
reportedHeights.append(height)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class KlarnaMessagingHeightObserverTests: XCTestCase {
|
|
13
|
+
|
|
14
|
+
var observer: KlarnaMessagingHeightObserver!
|
|
15
|
+
var delegate: MockHeightDelegate!
|
|
16
|
+
|
|
17
|
+
override func setUp() {
|
|
18
|
+
super.setUp()
|
|
19
|
+
delegate = MockHeightDelegate()
|
|
20
|
+
observer = KlarnaMessagingHeightObserver(delayIntervals: [])
|
|
21
|
+
observer.delegate = delegate
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override func tearDown() {
|
|
25
|
+
observer.stopObserving()
|
|
26
|
+
observer = nil
|
|
27
|
+
delegate = nil
|
|
28
|
+
super.tearDown()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// MARK: - Basic Observation
|
|
32
|
+
|
|
33
|
+
func testCheckAndReportHeight_doesNothingWithoutObservedView() {
|
|
34
|
+
observer.checkAndReportHeight()
|
|
35
|
+
XCTAssertTrue(delegate.reportedHeights.isEmpty)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func testCheckAndReportHeight_reportsHeightForViewWithFrame() {
|
|
39
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
40
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
41
|
+
container.addSubview(view)
|
|
42
|
+
|
|
43
|
+
observer.observe(view)
|
|
44
|
+
observer.checkAndReportHeight()
|
|
45
|
+
|
|
46
|
+
XCTAssertEqual(delegate.reportedHeights.count, 1)
|
|
47
|
+
XCTAssertEqual(delegate.reportedHeights.first, 100)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func testCheckAndReportHeight_doesNotReportZeroHeight() {
|
|
51
|
+
let view = UIView(frame: .zero)
|
|
52
|
+
observer.observe(view)
|
|
53
|
+
observer.checkAndReportHeight()
|
|
54
|
+
|
|
55
|
+
XCTAssertTrue(delegate.reportedHeights.isEmpty)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func testCheckAndReportHeight_doesNotReportSameHeightTwice() {
|
|
59
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
60
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 80))
|
|
61
|
+
container.addSubview(view)
|
|
62
|
+
|
|
63
|
+
observer.observe(view)
|
|
64
|
+
observer.checkAndReportHeight()
|
|
65
|
+
observer.checkAndReportHeight()
|
|
66
|
+
|
|
67
|
+
XCTAssertEqual(delegate.reportedHeights.count, 1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func testCheckAndReportHeight_reportsNewHeightAfterChange() {
|
|
71
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
72
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 80))
|
|
73
|
+
container.addSubview(view)
|
|
74
|
+
|
|
75
|
+
observer.observe(view)
|
|
76
|
+
observer.checkAndReportHeight()
|
|
77
|
+
|
|
78
|
+
view.frame = CGRect(x: 0, y: 0, width: 320, height: 120)
|
|
79
|
+
observer.checkAndReportHeight()
|
|
80
|
+
|
|
81
|
+
XCTAssertEqual(delegate.reportedHeights.count, 2)
|
|
82
|
+
XCTAssertEqual(delegate.reportedHeights[0], 80)
|
|
83
|
+
XCTAssertEqual(delegate.reportedHeights[1], 120)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - stopObserving
|
|
87
|
+
|
|
88
|
+
func testStopObserving_preventsSubsequentReports() {
|
|
89
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
90
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
91
|
+
container.addSubview(view)
|
|
92
|
+
|
|
93
|
+
observer.observe(view)
|
|
94
|
+
observer.stopObserving()
|
|
95
|
+
observer.checkAndReportHeight()
|
|
96
|
+
|
|
97
|
+
XCTAssertTrue(delegate.reportedHeights.isEmpty)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func testStopObserving_canBeCalledMultipleTimes() {
|
|
101
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
102
|
+
observer.observe(view)
|
|
103
|
+
observer.stopObserving()
|
|
104
|
+
observer.stopObserving() // Should not crash
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Guards against the KVO crash "instance was deallocated while key value
|
|
108
|
+
// observers were still registered". The observer must hold the observed
|
|
109
|
+
// view strongly while observation is active, otherwise the view can die
|
|
110
|
+
// before stopObserving runs.
|
|
111
|
+
func testObservedView_isRetainedWhileObservationIsActive() {
|
|
112
|
+
weak var weakView: UIView?
|
|
113
|
+
autoreleasepool {
|
|
114
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
115
|
+
weakView = view
|
|
116
|
+
observer.observe(view)
|
|
117
|
+
}
|
|
118
|
+
XCTAssertNotNil(weakView, "observer must keep the observed view alive while observing")
|
|
119
|
+
observer.stopObserving()
|
|
120
|
+
XCTAssertNil(weakView, "observed view must be released after stopObserving")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Re-observe
|
|
124
|
+
|
|
125
|
+
func testObserve_resetsLastReportedHeight() {
|
|
126
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
127
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
128
|
+
container.addSubview(view)
|
|
129
|
+
|
|
130
|
+
observer.observe(view)
|
|
131
|
+
observer.checkAndReportHeight()
|
|
132
|
+
XCTAssertEqual(delegate.reportedHeights.count, 1)
|
|
133
|
+
|
|
134
|
+
// Re-observe the same view — height should be reported again
|
|
135
|
+
observer.observe(view)
|
|
136
|
+
observer.checkAndReportHeight()
|
|
137
|
+
XCTAssertEqual(delegate.reportedHeights.count, 2)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - Delayed Checks
|
|
141
|
+
|
|
142
|
+
func testDelayedChecks_fireAfterIntervals() {
|
|
143
|
+
let delayObserver = KlarnaMessagingHeightObserver(delayIntervals: [0.1])
|
|
144
|
+
delayObserver.delegate = delegate
|
|
145
|
+
|
|
146
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
147
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 50))
|
|
148
|
+
container.addSubview(view)
|
|
149
|
+
|
|
150
|
+
delayObserver.observe(view)
|
|
151
|
+
|
|
152
|
+
let expectation = expectation(description: "Delayed height check fires")
|
|
153
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
154
|
+
expectation.fulfill()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
waitForExpectations(timeout: 1.0)
|
|
158
|
+
XCTAssertFalse(delegate.reportedHeights.isEmpty)
|
|
159
|
+
|
|
160
|
+
delayObserver.stopObserving()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MARK: - No Content (height 0 fallback)
|
|
164
|
+
|
|
165
|
+
func testReportsZeroHeight_whenAllDelaysFinishWithNoContent() {
|
|
166
|
+
let delayObserver = KlarnaMessagingHeightObserver(delayIntervals: [0.05, 0.1])
|
|
167
|
+
delayObserver.delegate = delegate
|
|
168
|
+
|
|
169
|
+
let view = UIView(frame: .zero)
|
|
170
|
+
delayObserver.observe(view)
|
|
171
|
+
|
|
172
|
+
let expectation = expectation(description: "All delayed checks finished")
|
|
173
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
174
|
+
expectation.fulfill()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
waitForExpectations(timeout: 1.0)
|
|
178
|
+
XCTAssertEqual(delegate.reportedHeights, [0])
|
|
179
|
+
|
|
180
|
+
delayObserver.stopObserving()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func testDoesNotReportZero_whenHeightIsPositive() {
|
|
184
|
+
let delayObserver = KlarnaMessagingHeightObserver(delayIntervals: [0.05, 0.1])
|
|
185
|
+
delayObserver.delegate = delegate
|
|
186
|
+
|
|
187
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
188
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 80))
|
|
189
|
+
container.addSubview(view)
|
|
190
|
+
|
|
191
|
+
delayObserver.observe(view)
|
|
192
|
+
|
|
193
|
+
let expectation = expectation(description: "All delayed checks finished")
|
|
194
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
195
|
+
expectation.fulfill()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
waitForExpectations(timeout: 1.0)
|
|
199
|
+
XCTAssertFalse(delegate.reportedHeights.isEmpty)
|
|
200
|
+
XCTAssertFalse(delegate.reportedHeights.contains(0))
|
|
201
|
+
|
|
202
|
+
delayObserver.stopObserving()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import KlarnaCore
|
|
3
|
+
import KlarnaNetworkMessaging
|
|
4
|
+
@testable import react_native_klarna_network_messaging
|
|
5
|
+
|
|
6
|
+
private let autoSize = PlacementType.autoSize.rawValue
|
|
7
|
+
private let badge = PlacementType.badge.rawValue
|
|
8
|
+
|
|
9
|
+
class KlarnaMessagingPlacementViewFactoryTests: XCTestCase {
|
|
10
|
+
|
|
11
|
+
// MARK: - parseTheme
|
|
12
|
+
|
|
13
|
+
func testParseTheme_lightMapsToLight() {
|
|
14
|
+
XCTAssertEqual(KlarnaMessagingPlacementViewFactory.parseTheme("light"), .light)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testParseTheme_darkMapsToDark() {
|
|
18
|
+
XCTAssertEqual(KlarnaMessagingPlacementViewFactory.parseTheme("dark"), .dark)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func testParseTheme_automaticMapsToAutomatic() {
|
|
22
|
+
XCTAssertEqual(KlarnaMessagingPlacementViewFactory.parseTheme("automatic"), .automatic)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func testParseTheme_unknownReturnsNil() {
|
|
26
|
+
XCTAssertNil(KlarnaMessagingPlacementViewFactory.parseTheme("neon"))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func testParseTheme_emptyReturnsNil() {
|
|
30
|
+
XCTAssertNil(KlarnaMessagingPlacementViewFactory.parseTheme(""))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func testParseTheme_isCaseSensitive() {
|
|
34
|
+
XCTAssertNil(KlarnaMessagingPlacementViewFactory.parseTheme("LIGHT"))
|
|
35
|
+
XCTAssertNil(KlarnaMessagingPlacementViewFactory.parseTheme("Dark"))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// MARK: - makeConfiguration
|
|
39
|
+
|
|
40
|
+
func testMakeConfiguration_badgeReturnsBadgeVariant() {
|
|
41
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
42
|
+
placementType: badge,
|
|
43
|
+
theme: .light,
|
|
44
|
+
amount: 1000,
|
|
45
|
+
currency: "USD"
|
|
46
|
+
)
|
|
47
|
+
if case .creditPromotionBadge = config {
|
|
48
|
+
// expected
|
|
49
|
+
} else {
|
|
50
|
+
XCTFail("Expected .creditPromotionBadge case, got \(config)")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func testMakeConfiguration_autoSizeReturnsAutoSizeVariant() {
|
|
55
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
56
|
+
placementType: autoSize,
|
|
57
|
+
theme: .dark,
|
|
58
|
+
amount: 2000,
|
|
59
|
+
currency: "EUR"
|
|
60
|
+
)
|
|
61
|
+
if case .creditPromotionAutoSize = config {
|
|
62
|
+
// expected
|
|
63
|
+
} else {
|
|
64
|
+
XCTFail("Expected .creditPromotionAutoSize case, got \(config)")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func testMakeConfiguration_unknownDefaultsToAutoSize() {
|
|
69
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
70
|
+
placementType: "SomethingElse",
|
|
71
|
+
theme: .automatic,
|
|
72
|
+
amount: 500,
|
|
73
|
+
currency: "SEK"
|
|
74
|
+
)
|
|
75
|
+
if case .creditPromotionAutoSize = config {
|
|
76
|
+
// expected
|
|
77
|
+
} else {
|
|
78
|
+
XCTFail("Expected .creditPromotionAutoSize fallback, got \(config)")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func testMakeConfiguration_emptyDefaultsToAutoSize() {
|
|
83
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
84
|
+
placementType: "",
|
|
85
|
+
theme: .automatic,
|
|
86
|
+
amount: 0,
|
|
87
|
+
currency: "GBP"
|
|
88
|
+
)
|
|
89
|
+
if case .creditPromotionAutoSize = config {
|
|
90
|
+
// expected
|
|
91
|
+
} else {
|
|
92
|
+
XCTFail("Expected .creditPromotionAutoSize fallback for empty string, got \(config)")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func testMakeConfiguration_nilThemeFlowsThroughToConfiguration() {
|
|
97
|
+
// When theme is nil the factory must not synthesize a default; the
|
|
98
|
+
// configuration must carry nil so the native SDK applies its own.
|
|
99
|
+
let autoSizeConfig = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
100
|
+
placementType: autoSize,
|
|
101
|
+
theme: nil,
|
|
102
|
+
amount: 1000,
|
|
103
|
+
currency: "USD"
|
|
104
|
+
)
|
|
105
|
+
if case .creditPromotionAutoSize(_, _, let theme) = autoSizeConfig {
|
|
106
|
+
XCTAssertNil(theme)
|
|
107
|
+
} else {
|
|
108
|
+
XCTFail("Expected .creditPromotionAutoSize, got \(autoSizeConfig)")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let badgeConfig = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
112
|
+
placementType: badge,
|
|
113
|
+
theme: nil,
|
|
114
|
+
amount: 1000,
|
|
115
|
+
currency: "USD"
|
|
116
|
+
)
|
|
117
|
+
if case .creditPromotionBadge(_, _, let theme) = badgeConfig {
|
|
118
|
+
XCTAssertNil(theme)
|
|
119
|
+
} else {
|
|
120
|
+
XCTFail("Expected .creditPromotionBadge, got \(badgeConfig)")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func testMakeConfiguration_explicitThemeFlowsThroughToConfiguration() {
|
|
125
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
126
|
+
placementType: autoSize,
|
|
127
|
+
theme: .dark,
|
|
128
|
+
amount: 1000,
|
|
129
|
+
currency: "USD"
|
|
130
|
+
)
|
|
131
|
+
if case .creditPromotionAutoSize(_, _, let theme) = config {
|
|
132
|
+
XCTAssertEqual(theme, .dark)
|
|
133
|
+
} else {
|
|
134
|
+
XCTFail("Expected .creditPromotionAutoSize, got \(config)")
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func testMakeConfiguration_caseSensitiveOnBadge() {
|
|
139
|
+
// Only exact "CreditPromotionBadge" returns the badge variant.
|
|
140
|
+
let config = KlarnaMessagingPlacementViewFactory.makeConfiguration(
|
|
141
|
+
placementType: "creditpromotionbadge",
|
|
142
|
+
theme: .automatic,
|
|
143
|
+
amount: 100,
|
|
144
|
+
currency: "NOK"
|
|
145
|
+
)
|
|
146
|
+
if case .creditPromotionAutoSize = config {
|
|
147
|
+
// expected
|
|
148
|
+
} else {
|
|
149
|
+
XCTFail("Expected .creditPromotionAutoSize for lowercase input, got \(config)")
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - createPlacementView
|
|
154
|
+
|
|
155
|
+
func testCreatePlacementView_returnsNilForInvalidKlarnaObject() {
|
|
156
|
+
// Any non-Klarna object should cause the factory to return nil rather than crash.
|
|
157
|
+
let invalidKlarna = NSObject()
|
|
158
|
+
let view = KlarnaMessagingPlacementViewFactory.createPlacementView(
|
|
159
|
+
klarna: invalidKlarna,
|
|
160
|
+
placementType: autoSize,
|
|
161
|
+
theme: "automatic",
|
|
162
|
+
amount: 1000,
|
|
163
|
+
currency: "USD"
|
|
164
|
+
)
|
|
165
|
+
XCTAssertNil(view)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import react_native_klarna_network_messaging
|
|
3
|
+
|
|
4
|
+
class KlarnaMessagingPlacementViewHelperTests: XCTestCase {
|
|
5
|
+
|
|
6
|
+
// MARK: - parseAmount
|
|
7
|
+
|
|
8
|
+
func testParseAmount_returnsNilForNil() {
|
|
9
|
+
XCTAssertNil(KlarnaMessagingPlacementViewHelper.parseAmount(nil))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func testParseAmount_returnsNilForEmpty() {
|
|
13
|
+
XCTAssertNil(KlarnaMessagingPlacementViewHelper.parseAmount(""))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testParseAmount_returnsNilForNonNumeric() {
|
|
17
|
+
XCTAssertNil(KlarnaMessagingPlacementViewHelper.parseAmount("abc"))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func testParseAmount_returnsNilForDecimal() {
|
|
21
|
+
XCTAssertNil(KlarnaMessagingPlacementViewHelper.parseAmount("12.5"))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func testParseAmount_returnsNilForOutOfRange() {
|
|
25
|
+
XCTAssertNil(KlarnaMessagingPlacementViewHelper.parseAmount("99999999999999999999"))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func testParseAmount_parsesValidInteger() {
|
|
29
|
+
XCTAssertEqual(KlarnaMessagingPlacementViewHelper.parseAmount("19900"), NSNumber(value: 19900))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testParseAmount_parsesNegativeInteger() {
|
|
33
|
+
XCTAssertEqual(KlarnaMessagingPlacementViewHelper.parseAmount("-500"), NSNumber(value: -500))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - calculateHeight
|
|
37
|
+
|
|
38
|
+
func testCalculateHeight_returnsZeroForEmptyView() {
|
|
39
|
+
let view = UIView(frame: .zero)
|
|
40
|
+
let height = KlarnaMessagingPlacementViewHelper.calculateHeight(for: view, containerWidth: 320)
|
|
41
|
+
XCTAssertEqual(height, 0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func testCalculateHeight_returnsFrameHeight() {
|
|
45
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 100))
|
|
46
|
+
let height = KlarnaMessagingPlacementViewHelper.calculateHeight(for: view, containerWidth: 320)
|
|
47
|
+
XCTAssertEqual(height, 100)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func testCalculateHeight_returnsSubviewMaxY() {
|
|
51
|
+
let view = UIView(frame: .zero)
|
|
52
|
+
let subview = UIView(frame: CGRect(x: 0, y: 10, width: 100, height: 50))
|
|
53
|
+
view.addSubview(subview)
|
|
54
|
+
let height = KlarnaMessagingPlacementViewHelper.calculateHeight(for: view, containerWidth: 320)
|
|
55
|
+
XCTAssertEqual(height, 60) // 10 + 50
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func testCalculateHeight_usesScreenWidthWhenContainerWidthIsZero() {
|
|
59
|
+
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 80))
|
|
60
|
+
let height = KlarnaMessagingPlacementViewHelper.calculateHeight(for: view, containerWidth: 0)
|
|
61
|
+
XCTAssertGreaterThanOrEqual(height, 0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MARK: - addPlacementSubview
|
|
65
|
+
|
|
66
|
+
func testAddPlacementSubview_addsSubview() {
|
|
67
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
68
|
+
let placement = UIView()
|
|
69
|
+
|
|
70
|
+
KlarnaMessagingPlacementViewHelper.addPlacementSubview(placement, to: container, pinWidth: false)
|
|
71
|
+
|
|
72
|
+
XCTAssertEqual(container.subviews.count, 1)
|
|
73
|
+
XCTAssertTrue(container.subviews.contains(placement))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func testAddPlacementSubview_disablesAutoresizingMask() {
|
|
77
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
78
|
+
let placement = UIView()
|
|
79
|
+
|
|
80
|
+
KlarnaMessagingPlacementViewHelper.addPlacementSubview(placement, to: container, pinWidth: false)
|
|
81
|
+
|
|
82
|
+
XCTAssertFalse(placement.translatesAutoresizingMaskIntoConstraints)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func testAddPlacementSubview_addsTopLeadingTrailingConstraints() {
|
|
86
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
87
|
+
let placement = UIView()
|
|
88
|
+
|
|
89
|
+
KlarnaMessagingPlacementViewHelper.addPlacementSubview(placement, to: container, pinWidth: false)
|
|
90
|
+
|
|
91
|
+
// Should have at least top, leading, trailing constraints
|
|
92
|
+
let constraints = container.constraints.filter { constraint in
|
|
93
|
+
constraint.firstItem === placement || constraint.secondItem === placement
|
|
94
|
+
}
|
|
95
|
+
XCTAssertGreaterThanOrEqual(constraints.count, 3)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func testAddPlacementSubview_pinsWidthWhenRequested() {
|
|
99
|
+
let container = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200))
|
|
100
|
+
let placement = UIView()
|
|
101
|
+
|
|
102
|
+
KlarnaMessagingPlacementViewHelper.addPlacementSubview(placement, to: container, pinWidth: true)
|
|
103
|
+
|
|
104
|
+
let widthConstraints = placement.constraints.filter { $0.firstAttribute == .width }
|
|
105
|
+
XCTAssertEqual(widthConstraints.count, 1)
|
|
106
|
+
XCTAssertEqual(widthConstraints.first?.constant, 320)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func testAddPlacementSubview_doesNotPinWidthWhenContainerHasZeroWidth() {
|
|
110
|
+
let container = UIView(frame: .zero)
|
|
111
|
+
let placement = UIView()
|
|
112
|
+
|
|
113
|
+
KlarnaMessagingPlacementViewHelper.addPlacementSubview(placement, to: container, pinWidth: true)
|
|
114
|
+
|
|
115
|
+
let widthConstraints = placement.constraints.filter { $0.firstAttribute == .width }
|
|
116
|
+
XCTAssertEqual(widthConstraints.count, 0)
|
|
117
|
+
}
|
|
118
|
+
}
|