@klarna/react-native-klarna-network-messaging 0.0.1 → 2.7.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.
Files changed (72) hide show
  1. package/README.md +80 -28
  2. package/android/build.gradle +97 -0
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementErrorEvent.kt +25 -0
  5. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementResizedEvent.kt +20 -0
  6. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementViewManager.kt +251 -0
  7. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingViewManagerHelpers.kt +52 -0
  8. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaNetworkMessagingPackage.kt +19 -0
  9. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/RNKlarnaMessagingPlacementViewSpec.kt +22 -0
  10. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementEventsTest.kt +39 -0
  11. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingPlacementViewManagerTest.kt +76 -0
  12. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/klarnanetwork/messaging/KlarnaMessagingViewManagerHelpersTest.kt +200 -0
  13. package/ios/Sources/KlarnaMessagingPlacementViewFactory.swift +101 -0
  14. package/ios/Sources/KlarnaMessagingPlacementViewHelper.swift +171 -0
  15. package/ios/Sources/KlarnaMessagingPlacementViewManager.h +5 -0
  16. package/ios/Sources/KlarnaMessagingPlacementViewManager.mm +37 -0
  17. package/ios/Sources/view/KlarnaMessagingPlacementViewWrapper.h +7 -0
  18. package/ios/Sources/view/KlarnaMessagingPlacementViewWrapper.mm +183 -0
  19. package/ios/Tests/KlarnaMessagingHeightObserverTests.swift +204 -0
  20. package/ios/Tests/KlarnaMessagingPlacementViewFactoryTests.swift +167 -0
  21. package/ios/Tests/KlarnaMessagingPlacementViewHelperTests.swift +118 -0
  22. package/lib/commonjs/KlarnaMessagingPlacementView.js +102 -0
  23. package/lib/commonjs/KlarnaMessagingPlacementView.js.map +1 -0
  24. package/lib/commonjs/Specs/KlarnaMessagingPlacementViewNativeComponent.js +10 -0
  25. package/lib/commonjs/Specs/KlarnaMessagingPlacementViewNativeComponent.js.map +1 -0
  26. package/lib/commonjs/index.js +27 -0
  27. package/lib/commonjs/index.js.map +1 -0
  28. package/lib/commonjs/internal.js +45 -0
  29. package/lib/commonjs/internal.js.map +1 -0
  30. package/lib/commonjs/types/KlarnaMessagingError.js +2 -0
  31. package/lib/commonjs/types/KlarnaMessagingError.js.map +1 -0
  32. package/lib/commonjs/types/KlarnaMessagingPlacementConfiguration.js +6 -0
  33. package/lib/commonjs/types/KlarnaMessagingPlacementConfiguration.js.map +1 -0
  34. package/lib/commonjs/types/KlarnaMessagingPlacementType.js +15 -0
  35. package/lib/commonjs/types/KlarnaMessagingPlacementType.js.map +1 -0
  36. package/lib/module/KlarnaMessagingPlacementView.js +94 -0
  37. package/lib/module/KlarnaMessagingPlacementView.js.map +1 -0
  38. package/lib/module/Specs/KlarnaMessagingPlacementViewNativeComponent.js +3 -0
  39. package/lib/module/Specs/KlarnaMessagingPlacementViewNativeComponent.js.map +1 -0
  40. package/lib/module/index.js +4 -0
  41. package/lib/module/index.js.map +1 -0
  42. package/lib/module/internal.js +38 -0
  43. package/lib/module/internal.js.map +1 -0
  44. package/lib/module/types/KlarnaMessagingError.js +2 -0
  45. package/lib/module/types/KlarnaMessagingError.js.map +1 -0
  46. package/lib/module/types/KlarnaMessagingPlacementConfiguration.js +2 -0
  47. package/lib/module/types/KlarnaMessagingPlacementConfiguration.js.map +1 -0
  48. package/lib/module/types/KlarnaMessagingPlacementType.js +9 -0
  49. package/lib/module/types/KlarnaMessagingPlacementType.js.map +1 -0
  50. package/lib/typescript/KlarnaMessagingPlacementView.d.ts +39 -0
  51. package/lib/typescript/KlarnaMessagingPlacementView.d.ts.map +1 -0
  52. package/lib/typescript/Specs/KlarnaMessagingPlacementViewNativeComponent.d.ts +23 -0
  53. package/lib/typescript/Specs/KlarnaMessagingPlacementViewNativeComponent.d.ts.map +1 -0
  54. package/lib/typescript/index.d.ts +6 -0
  55. package/lib/typescript/index.d.ts.map +1 -0
  56. package/lib/typescript/internal.d.ts +25 -0
  57. package/lib/typescript/internal.d.ts.map +1 -0
  58. package/lib/typescript/types/KlarnaMessagingError.d.ts +8 -0
  59. package/lib/typescript/types/KlarnaMessagingError.d.ts.map +1 -0
  60. package/lib/typescript/types/KlarnaMessagingPlacementConfiguration.d.ts +25 -0
  61. package/lib/typescript/types/KlarnaMessagingPlacementConfiguration.d.ts.map +1 -0
  62. package/lib/typescript/types/KlarnaMessagingPlacementType.d.ts +8 -0
  63. package/lib/typescript/types/KlarnaMessagingPlacementType.d.ts.map +1 -0
  64. package/package.json +122 -6
  65. package/react-native-klarna-network-messaging.podspec +28 -0
  66. package/src/KlarnaMessagingPlacementView.tsx +139 -0
  67. package/src/Specs/KlarnaMessagingPlacementViewNativeComponent.ts +32 -0
  68. package/src/index.tsx +12 -0
  69. package/src/internal.ts +48 -0
  70. package/src/types/KlarnaMessagingError.ts +7 -0
  71. package/src/types/KlarnaMessagingPlacementConfiguration.ts +29 -0
  72. 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
+ }