react-native-klarna-inapp-sdk 2.4.9 → 2.5.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 (121) hide show
  1. package/README.md +2 -1
  2. package/android/build.gradle +3 -1
  3. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/KlarnaMobileSDKPackage.java +5 -1
  4. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/checkout/KlarnaCheckoutViewManager.java +4 -10
  5. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/WebViewResizeObserver.java +28 -0
  6. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/event/ComponentEventSender.java +5 -1
  7. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/ResizeObserverWrapperView.java +2 -2
  8. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/WrapperView.java +2 -2
  9. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/YogaLayoutHelper.java +39 -0
  10. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewCallback.kt +30 -0
  11. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewEventSender.kt +80 -0
  12. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewManager.java +544 -0
  13. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventHandler.java +30 -0
  14. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventSender.java +64 -0
  15. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewManager.java +356 -0
  16. package/android/src/newarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaExpressCheckoutViewSpec.java +31 -0
  17. package/android/src/newarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaOSMViewSpec.java +31 -0
  18. package/android/src/oldarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaExpressCheckoutViewSpec.java +36 -0
  19. package/android/src/oldarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaOSMViewSpec.java +32 -0
  20. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/KlarnaMobileSDKPackageTest.java +5 -1
  21. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/common/ui/YogaLayoutHelperTest.java +79 -0
  22. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewManagerTest.java +259 -0
  23. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventHandlerTest.java +136 -0
  24. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventSenderTest.java +184 -0
  25. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewManagerTest.java +385 -0
  26. package/ios/RNKlarnaMobileSDK.xcodeproj/project.pbxproj +55 -1
  27. package/ios/Sources/KlarnaExpressCheckoutViewManager.h +5 -0
  28. package/ios/Sources/KlarnaExpressCheckoutViewManager.mm +39 -0
  29. package/ios/Sources/KlarnaOSMViewManager.h +5 -0
  30. package/ios/Sources/KlarnaOSMViewManager.mm +59 -0
  31. package/ios/Sources/view/KlarnaExpressCheckoutButtonDelegateProxy.swift +145 -0
  32. package/ios/Sources/view/KlarnaExpressCheckoutViewWrapper.h +41 -0
  33. package/ios/Sources/view/KlarnaOSMViewWrapper.h +48 -0
  34. package/ios/Sources/view/newarch/KlarnaExpressCheckoutViewWrapper.mm +228 -0
  35. package/ios/Sources/view/newarch/KlarnaOSMViewWrapper.mm +294 -0
  36. package/ios/Sources/view/newarch/KlarnaStandaloneWebViewWrapper.mm +20 -13
  37. package/ios/Sources/view/oldarch/KlarnaExpressCheckoutViewWrapper.mm +222 -0
  38. package/ios/Sources/view/oldarch/KlarnaOSMViewWrapper.mm +273 -0
  39. package/ios/Sources/view/oldarch/KlarnaStandaloneWebViewWrapper.mm +11 -13
  40. package/ios/Tests/KlarnaExpressCheckoutViewWrapperTests.m +391 -0
  41. package/ios/Tests/KlarnaOSMViewWrapperTests.m +450 -0
  42. package/ios/Tests/KlarnaStandaloneWebViewWrapperTests.m +105 -0
  43. package/lib/commonjs/KlarnaExpressCheckoutView.js +75 -0
  44. package/lib/commonjs/KlarnaExpressCheckoutView.js.map +1 -0
  45. package/lib/commonjs/KlarnaOSMView.js +94 -0
  46. package/lib/commonjs/KlarnaOSMView.js.map +1 -0
  47. package/lib/commonjs/index.js +99 -0
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/specs/KlarnaExpressCheckoutViewNativeComponent.js +11 -0
  50. package/lib/commonjs/specs/KlarnaExpressCheckoutViewNativeComponent.js.map +1 -0
  51. package/lib/commonjs/specs/KlarnaOSMViewNativeComponent.js +16 -0
  52. package/lib/commonjs/specs/KlarnaOSMViewNativeComponent.js.map +1 -0
  53. package/lib/commonjs/types/button/KlarnaButtonShape.js +14 -0
  54. package/lib/commonjs/types/button/KlarnaButtonShape.js.map +1 -0
  55. package/lib/commonjs/types/button/KlarnaButtonStyle.js +13 -0
  56. package/lib/commonjs/types/button/KlarnaButtonStyle.js.map +1 -0
  57. package/lib/commonjs/types/button/KlarnaButtonTheme.js +14 -0
  58. package/lib/commonjs/types/button/KlarnaButtonTheme.js.map +1 -0
  59. package/lib/commonjs/types/common/KlarnaExpressCheckoutSessionOptions.js +9 -0
  60. package/lib/commonjs/types/common/KlarnaExpressCheckoutSessionOptions.js.map +1 -0
  61. package/lib/commonjs/types/common/KlarnaOSMPlacementKey.js +2 -0
  62. package/lib/commonjs/types/common/KlarnaOSMPlacementKey.js.map +1 -0
  63. package/lib/commonjs/types/common/KlarnaTheme.js +14 -0
  64. package/lib/commonjs/types/common/KlarnaTheme.js.map +1 -0
  65. package/lib/module/KlarnaExpressCheckoutView.js +64 -0
  66. package/lib/module/KlarnaExpressCheckoutView.js.map +1 -0
  67. package/lib/module/KlarnaOSMView.js +84 -0
  68. package/lib/module/KlarnaOSMView.js.map +1 -0
  69. package/lib/module/index.js +9 -0
  70. package/lib/module/index.js.map +1 -1
  71. package/lib/module/specs/KlarnaExpressCheckoutViewNativeComponent.js +3 -0
  72. package/lib/module/specs/KlarnaExpressCheckoutViewNativeComponent.js.map +1 -0
  73. package/lib/module/specs/KlarnaOSMViewNativeComponent.js +7 -0
  74. package/lib/module/specs/KlarnaOSMViewNativeComponent.js.map +1 -0
  75. package/lib/module/types/button/KlarnaButtonShape.js +7 -0
  76. package/lib/module/types/button/KlarnaButtonShape.js.map +1 -0
  77. package/lib/module/types/button/KlarnaButtonStyle.js +6 -0
  78. package/lib/module/types/button/KlarnaButtonStyle.js.map +1 -0
  79. package/lib/module/types/button/KlarnaButtonTheme.js +7 -0
  80. package/lib/module/types/button/KlarnaButtonTheme.js.map +1 -0
  81. package/lib/module/types/common/KlarnaExpressCheckoutSessionOptions.js +2 -0
  82. package/lib/module/types/common/KlarnaExpressCheckoutSessionOptions.js.map +1 -0
  83. package/lib/module/types/common/KlarnaOSMPlacementKey.js +2 -0
  84. package/lib/module/types/common/KlarnaOSMPlacementKey.js.map +1 -0
  85. package/lib/module/types/common/KlarnaTheme.js +7 -0
  86. package/lib/module/types/common/KlarnaTheme.js.map +1 -0
  87. package/lib/typescript/KlarnaExpressCheckoutView.d.ts +47 -0
  88. package/lib/typescript/KlarnaExpressCheckoutView.d.ts.map +1 -0
  89. package/lib/typescript/KlarnaOSMView.d.ts +36 -0
  90. package/lib/typescript/KlarnaOSMView.d.ts.map +1 -0
  91. package/lib/typescript/index.d.ts +9 -0
  92. package/lib/typescript/index.d.ts.map +1 -1
  93. package/lib/typescript/specs/KlarnaExpressCheckoutViewNativeComponent.d.ts +45 -0
  94. package/lib/typescript/specs/KlarnaExpressCheckoutViewNativeComponent.d.ts.map +1 -0
  95. package/lib/typescript/specs/KlarnaOSMViewNativeComponent.d.ts +35 -0
  96. package/lib/typescript/specs/KlarnaOSMViewNativeComponent.d.ts.map +1 -0
  97. package/lib/typescript/types/button/KlarnaButtonShape.d.ts +6 -0
  98. package/lib/typescript/types/button/KlarnaButtonShape.d.ts.map +1 -0
  99. package/lib/typescript/types/button/KlarnaButtonStyle.d.ts +5 -0
  100. package/lib/typescript/types/button/KlarnaButtonStyle.d.ts.map +1 -0
  101. package/lib/typescript/types/button/KlarnaButtonTheme.d.ts +6 -0
  102. package/lib/typescript/types/button/KlarnaButtonTheme.d.ts.map +1 -0
  103. package/lib/typescript/types/common/KlarnaExpressCheckoutSessionOptions.d.ts +10 -0
  104. package/lib/typescript/types/common/KlarnaExpressCheckoutSessionOptions.d.ts.map +1 -0
  105. package/lib/typescript/types/common/KlarnaOSMPlacementKey.d.ts +2 -0
  106. package/lib/typescript/types/common/KlarnaOSMPlacementKey.d.ts.map +1 -0
  107. package/lib/typescript/types/common/KlarnaTheme.d.ts +6 -0
  108. package/lib/typescript/types/common/KlarnaTheme.d.ts.map +1 -0
  109. package/package.json +8 -3
  110. package/react-native-klarna-inapp-sdk.podspec +2 -1
  111. package/src/KlarnaExpressCheckoutView.tsx +169 -0
  112. package/src/KlarnaOSMView.tsx +157 -0
  113. package/src/index.tsx +9 -0
  114. package/src/specs/KlarnaExpressCheckoutViewNativeComponent.ts +66 -0
  115. package/src/specs/KlarnaOSMViewNativeComponent.ts +50 -0
  116. package/src/types/button/KlarnaButtonShape.ts +5 -0
  117. package/src/types/button/KlarnaButtonStyle.ts +4 -0
  118. package/src/types/button/KlarnaButtonTheme.ts +5 -0
  119. package/src/types/common/KlarnaExpressCheckoutSessionOptions.ts +13 -0
  120. package/src/types/common/KlarnaOSMPlacementKey.ts +3 -0
  121. package/src/types/common/KlarnaTheme.ts +5 -0
@@ -0,0 +1,391 @@
1
+ //
2
+ // KlarnaExpressCheckoutViewWrapperTests.m
3
+ // RNKlarnaMobileSDKTests
4
+ //
5
+ // Tests for the old-arch KlarnaExpressCheckoutViewWrapper (UIView-based).
6
+ //
7
+ // Coverage:
8
+ // • Initial state after -init
9
+ // • All React-Native prop setters store their values
10
+ // • -didSetProps: calls -resetButton then -createButtonIfNeeded when button
11
+ // already exists, or just -createButtonIfNeeded on first render
12
+ // • -resetButton removes the button subview, nils the reference, clears
13
+ // the isButtonCreated flag
14
+ // • -createButtonIfNeeded exits early (no SDK call) when isButtonCreated==YES
15
+ // • -didMoveToWindow with nil window resets the recreation guard
16
+ // • Delegate-proxy handler closures forward events to the React Native blocks
17
+ //
18
+
19
+ #import <XCTest/XCTest.h>
20
+ #import <OCMock/OCMock.h>
21
+ #import <KlarnaMobileSDK/KlarnaMobileSDK.h>
22
+ #import <KlarnaMobileSDK/KlarnaMobileSDK-Swift.h>
23
+
24
+ #import "../Sources/view/KlarnaExpressCheckoutViewWrapper.h"
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Expose private state for white-box testing.
28
+ // delegateProxy is typed as `id` to avoid importing the Swift bridge header
29
+ // into the test target; handler blocks are accessed via KVC below.
30
+ // ---------------------------------------------------------------------------
31
+ @interface KlarnaExpressCheckoutViewWrapper ()
32
+
33
+ @property (nonatomic, strong) KlarnaExpressCheckoutButton *expressCheckoutButton;
34
+ @property (nonatomic, strong) id delegateProxy;
35
+ @property (nonatomic, assign) BOOL isButtonCreated;
36
+
37
+ - (void)createButtonIfNeeded;
38
+ - (void)resetButton;
39
+
40
+ @end
41
+
42
+ // ---------------------------------------------------------------------------
43
+
44
+ @interface KlarnaExpressCheckoutViewWrapperTests : XCTestCase
45
+
46
+ @property (nonatomic, strong) KlarnaExpressCheckoutViewWrapper *wrapper;
47
+ /// OCMPartialMock wrapping wrapper — used to stub / verify internal method calls.
48
+ @property (nonatomic, strong) id wrapperMock;
49
+
50
+ @end
51
+
52
+ @implementation KlarnaExpressCheckoutViewWrapperTests
53
+
54
+ - (void)setUp {
55
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
56
+ self.wrapper = w;
57
+ self.wrapperMock = OCMPartialMock(w);
58
+ }
59
+
60
+ - (void)tearDown {
61
+ [self.wrapperMock stopMocking];
62
+ self.wrapperMock = nil;
63
+ self.wrapper = nil;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ #pragma mark - Initial State
68
+ // ---------------------------------------------------------------------------
69
+
70
+ - (void)test_initialState_buttonNotCreated {
71
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
72
+ XCTAssertFalse(w.isButtonCreated);
73
+ XCTAssertNil(w.expressCheckoutButton);
74
+ }
75
+
76
+ - (void)test_initialState_delegateProxyIsConfigured {
77
+ // setupDelegateProxy must run during -init and install both handlers.
78
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
79
+ XCTAssertNotNil(w.delegateProxy);
80
+
81
+ void (^authHandler)(NSDictionary *) = [w.delegateProxy valueForKey:@"onAuthorizedHandler"];
82
+ void (^errHandler)(NSString *, NSString *, BOOL) = [w.delegateProxy valueForKey:@"onErrorHandler"];
83
+ XCTAssertNotNil(authHandler, @"onAuthorizedHandler must be wired during -init");
84
+ XCTAssertNotNil(errHandler, @"onErrorHandler must be wired during -init");
85
+ }
86
+
87
+ - (void)test_initialState_autoFinalizeDefaultsToYES {
88
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
89
+ XCTAssertTrue(w.autoFinalize);
90
+ }
91
+
92
+ - (void)test_initialState_collectShippingAddressDefaultsToNO {
93
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
94
+ XCTAssertFalse(w.collectShippingAddress);
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ #pragma mark - Prop Storage
99
+ // ---------------------------------------------------------------------------
100
+
101
+ - (void)test_setClientId_storesValue {
102
+ [self.wrapper setClientId:@"client-abc"];
103
+ XCTAssertEqualObjects(self.wrapper.clientId, @"client-abc");
104
+ }
105
+
106
+ - (void)test_setClientToken_storesValue {
107
+ [self.wrapper setClientToken:@"token-xyz"];
108
+ XCTAssertEqualObjects(self.wrapper.clientToken, @"token-xyz");
109
+ }
110
+
111
+ - (void)test_setReturnUrl_storesValue {
112
+ [self.wrapper setReturnUrl:@"testapp://checkout/return"];
113
+ XCTAssertEqualObjects(self.wrapper.returnUrl, @"testapp://checkout/return");
114
+ }
115
+
116
+ - (void)test_setLocale_storesValue {
117
+ [self.wrapper setLocale:@"sv-SE"];
118
+ XCTAssertEqualObjects(self.wrapper.locale, @"sv-SE");
119
+ }
120
+
121
+ - (void)test_setEnvironment_storesValue {
122
+ [self.wrapper setEnvironment:@"playground"];
123
+ XCTAssertEqualObjects(self.wrapper.environment, @"playground");
124
+ }
125
+
126
+ - (void)test_setRegion_storesValue {
127
+ [self.wrapper setRegion:@"na"];
128
+ XCTAssertEqualObjects(self.wrapper.region, @"na");
129
+ }
130
+
131
+ - (void)test_setTheme_storesValue {
132
+ [self.wrapper setTheme:@"dark"];
133
+ XCTAssertEqualObjects(self.wrapper.theme, @"dark");
134
+ }
135
+
136
+ - (void)test_setShape_storesValue {
137
+ [self.wrapper setShape:@"pill"];
138
+ XCTAssertEqualObjects(self.wrapper.shape, @"pill");
139
+ }
140
+
141
+ - (void)test_setButtonStyle_storesValue {
142
+ [self.wrapper setButtonStyle:@"outlined"];
143
+ XCTAssertEqualObjects(self.wrapper.buttonStyle, @"outlined");
144
+ }
145
+
146
+ - (void)test_setAutoFinalize_storesNO {
147
+ [self.wrapper setAutoFinalize:NO];
148
+ XCTAssertFalse(self.wrapper.autoFinalize);
149
+ }
150
+
151
+ - (void)test_setCollectShippingAddress_storesYES {
152
+ [self.wrapper setCollectShippingAddress:YES];
153
+ XCTAssertTrue(self.wrapper.collectShippingAddress);
154
+ }
155
+
156
+ - (void)test_setSessionData_storesValue {
157
+ [self.wrapper setSessionData:@"{\"key\":\"val\"}"];
158
+ XCTAssertEqualObjects(self.wrapper.sessionData, @"{\"key\":\"val\"}");
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ #pragma mark - didSetProps
163
+ // ---------------------------------------------------------------------------
164
+
165
+ - (void)test_didSetProps_callsCreateButtonIfNeeded_onFirstRender {
166
+ // isButtonCreated starts as NO — only createButtonIfNeeded should be invoked.
167
+ OCMExpect([self.wrapperMock createButtonIfNeeded]);
168
+
169
+ [self.wrapper didSetProps:@[]];
170
+
171
+ OCMVerifyAll(self.wrapperMock);
172
+ }
173
+
174
+ - (void)test_didSetProps_whenButtonAlreadyCreated_resetsBeforeRecreating {
175
+ // Every prop change while a button exists must remove it before recreating.
176
+ self.wrapper.isButtonCreated = YES;
177
+
178
+ // resetButton must run with its real implementation (clearing the flag).
179
+ OCMExpect([self.wrapperMock resetButton]).andForwardToRealObject();
180
+ // createButtonIfNeeded is a no-op here to avoid real SDK calls.
181
+ OCMExpect([self.wrapperMock createButtonIfNeeded]);
182
+
183
+ [self.wrapper didSetProps:@[@"clientToken"]];
184
+
185
+ OCMVerifyAll(self.wrapperMock);
186
+ // resetButton must have cleared the flag to prevent stale-button leaks.
187
+ XCTAssertFalse(self.wrapper.isButtonCreated);
188
+ }
189
+
190
+ - (void)test_didSetProps_whenButtonNotCreated_skipsReset {
191
+ // resetButton must NOT be called on the initial render pass.
192
+ self.wrapper.isButtonCreated = NO;
193
+
194
+ OCMReject([self.wrapperMock resetButton]);
195
+ OCMExpect([self.wrapperMock createButtonIfNeeded]);
196
+
197
+ [self.wrapper didSetProps:@[]];
198
+
199
+ OCMVerifyAll(self.wrapperMock);
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ #pragma mark - resetButton
204
+ // ---------------------------------------------------------------------------
205
+
206
+ - (void)test_resetButton_clearsIsButtonCreated {
207
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
208
+ w.isButtonCreated = YES;
209
+
210
+ [w resetButton];
211
+
212
+ XCTAssertFalse(w.isButtonCreated);
213
+ }
214
+
215
+ - (void)test_resetButton_nilsButtonReference {
216
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
217
+ id mockButton = OCMClassMock([KlarnaExpressCheckoutButton class]);
218
+ w.expressCheckoutButton = mockButton;
219
+ w.isButtonCreated = YES;
220
+
221
+ [w resetButton];
222
+
223
+ XCTAssertNil(w.expressCheckoutButton);
224
+ }
225
+
226
+ - (void)test_resetButton_callsRemoveFromSuperview {
227
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
228
+ id mockButton = OCMClassMock([KlarnaExpressCheckoutButton class]);
229
+ OCMExpect([mockButton removeFromSuperview]);
230
+ w.expressCheckoutButton = mockButton;
231
+ w.isButtonCreated = YES;
232
+
233
+ [w resetButton];
234
+
235
+ OCMVerifyAll(mockButton);
236
+ }
237
+
238
+ - (void)test_resetButton_withNoExistingButton_doesNotCrash {
239
+ // Called when isButtonCreated==NO and expressCheckoutButton==nil — must be safe.
240
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
241
+ XCTAssertNoThrow([w resetButton]);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ #pragma mark - createButtonIfNeeded guard
246
+ // ---------------------------------------------------------------------------
247
+
248
+ - (void)test_createButtonIfNeeded_whenAlreadyCreated_isNoOp {
249
+ // The early-exit guard must prevent double-creation on rapid successive calls.
250
+ // If it were to fall through it would attempt real SDK initialisation and crash.
251
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
252
+ w.isButtonCreated = YES;
253
+
254
+ [w createButtonIfNeeded];
255
+
256
+ XCTAssertTrue(w.isButtonCreated, @"isButtonCreated must stay YES — guard failed");
257
+ XCTAssertNil(w.expressCheckoutButton, @"no button should be created by a no-op path");
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ #pragma mark - didMoveToWindow Lifecycle
262
+ // ---------------------------------------------------------------------------
263
+
264
+ - (void)test_didMoveToWindow_withNilWindow_resetsIsButtonCreated {
265
+ // Removal from the view hierarchy must clear the recreation guard so that
266
+ // re-insertion triggers a fresh button build.
267
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
268
+ w.isButtonCreated = YES;
269
+
270
+ // Wrapper is not attached to any UIWindow → window == nil.
271
+ [w didMoveToWindow];
272
+
273
+ XCTAssertFalse(w.isButtonCreated);
274
+ }
275
+
276
+ - (void)test_didMoveToWindow_withNilWindow_clearsButtonReference {
277
+ // resetButton is called when the view is detached, which nils the button
278
+ // reference and clears the recreation guard.
279
+ KlarnaExpressCheckoutViewWrapper *w = [KlarnaExpressCheckoutViewWrapper new];
280
+ id mockButton = OCMClassMock([KlarnaExpressCheckoutButton class]);
281
+ w.expressCheckoutButton = mockButton;
282
+ w.isButtonCreated = YES;
283
+
284
+ [w didMoveToWindow];
285
+
286
+ XCTAssertNil(w.expressCheckoutButton);
287
+ XCTAssertFalse(w.isButtonCreated);
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ #pragma mark - Delegate Proxy Event Forwarding
292
+ //
293
+ // The proxy handler blocks (installed by -setupDelegateProxy) are accessed via
294
+ // KVC to avoid importing the Swift bridge header into the test target.
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /// Simulates the SDK calling the authorized delegate method by invoking the
298
+ /// closure that -setupDelegateProxy installed on the delegate proxy.
299
+ - (void)simulateAuthorizedEvent:(NSDictionary *)response {
300
+ void (^handler)(NSDictionary *) = [self.wrapper.delegateProxy valueForKey:@"onAuthorizedHandler"];
301
+ XCTAssertNotNil(handler, @"onAuthorizedHandler must be wired up by setupDelegateProxy");
302
+ handler(response);
303
+ }
304
+
305
+ /// Simulates the SDK calling the error delegate method.
306
+ - (void)simulateErrorEventWithName:(NSString *)name
307
+ message:(NSString *)message
308
+ isFatal:(BOOL)isFatal {
309
+ void (^handler)(NSString *, NSString *, BOOL) =
310
+ [self.wrapper.delegateProxy valueForKey:@"onErrorHandler"];
311
+ XCTAssertNotNil(handler, @"onErrorHandler must be wired up by setupDelegateProxy");
312
+ handler(name, message, isFatal);
313
+ }
314
+
315
+ - (void)test_onAuthorized_forwardsFullResponseToReactBlock {
316
+ __block NSDictionary *receivedBody = nil;
317
+ [self.wrapper setOnAuthorized:^(NSDictionary *body) {
318
+ receivedBody = body;
319
+ }];
320
+
321
+ NSDictionary *authResponse = @{
322
+ @"showForm": @YES,
323
+ @"approved": @YES,
324
+ @"finalizedRequired": @NO,
325
+ @"clientToken": @"ct-123",
326
+ @"authorizationToken": @"at-456",
327
+ @"sessionId": @"sid-789",
328
+ @"collectedShippingAddress": @"",
329
+ @"merchantReference1": @"ref1",
330
+ @"merchantReference2": @"ref2",
331
+ };
332
+
333
+ [self simulateAuthorizedEvent:authResponse];
334
+
335
+ XCTAssertNotNil(receivedBody);
336
+ NSDictionary *result = receivedBody[@"authorizationResponse"];
337
+ XCTAssertEqualObjects(result[@"clientToken"], @"ct-123");
338
+ XCTAssertEqualObjects(result[@"authorizationToken"], @"at-456");
339
+ XCTAssertEqualObjects(result[@"sessionId"], @"sid-789");
340
+ XCTAssertEqualObjects(result[@"approved"], @YES);
341
+ XCTAssertEqualObjects(result[@"showForm"], @YES);
342
+ XCTAssertEqualObjects(result[@"finalizedRequired"], @NO);
343
+ XCTAssertEqualObjects(result[@"merchantReference1"], @"ref1");
344
+ XCTAssertEqualObjects(result[@"merchantReference2"], @"ref2");
345
+ }
346
+
347
+ - (void)test_onAuthorized_withNilCallback_doesNotCrash {
348
+ // The handler must guard against nil onAuthorized to prevent a crash when
349
+ // the JS side has not registered a listener yet.
350
+ XCTAssertNoThrow([self simulateAuthorizedEvent:@{}]);
351
+ }
352
+
353
+ - (void)test_onError_forwardsFatalErrorToReactBlock {
354
+ __block NSDictionary *receivedBody = nil;
355
+ [self.wrapper setOnError:^(NSDictionary *body) {
356
+ receivedBody = body;
357
+ }];
358
+
359
+ [self simulateErrorEventWithName:@"NetworkError"
360
+ message:@"Request timed out"
361
+ isFatal:YES];
362
+
363
+ XCTAssertNotNil(receivedBody);
364
+ NSDictionary *error = receivedBody[@"error"];
365
+ XCTAssertEqualObjects(error[@"name"], @"NetworkError");
366
+ XCTAssertEqualObjects(error[@"message"], @"Request timed out");
367
+ XCTAssertEqualObjects(error[@"isFatal"], @YES);
368
+ }
369
+
370
+ - (void)test_onError_nonFatalError_propagatesIsFatalNO {
371
+ __block NSDictionary *receivedBody = nil;
372
+ [self.wrapper setOnError:^(NSDictionary *body) {
373
+ receivedBody = body;
374
+ }];
375
+
376
+ [self simulateErrorEventWithName:@"ValidationError"
377
+ message:@"Invalid card number"
378
+ isFatal:NO];
379
+
380
+ XCTAssertEqualObjects(receivedBody[@"error"][@"isFatal"], @NO);
381
+ }
382
+
383
+ - (void)test_onError_withNilCallback_doesNotCrash {
384
+ // The handler must guard against nil onError to prevent a crash when the
385
+ // JS side has not registered a listener yet.
386
+ XCTAssertNoThrow([self simulateErrorEventWithName:@"err"
387
+ message:@"msg"
388
+ isFatal:NO]);
389
+ }
390
+
391
+ @end