react-native-klarna-inapp-sdk 2.4.10 → 2.5.1

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 (167) hide show
  1. package/android/build.gradle +3 -1
  2. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/KlarnaMobileSDKPackage.java +5 -1
  3. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/checkout/KlarnaCheckoutViewManager.java +4 -10
  4. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/WebViewResizeObserver.java +28 -0
  5. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/event/ComponentEventSender.java +5 -1
  6. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/ResizeObserverWrapperView.java +2 -2
  7. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/WrapperView.java +2 -2
  8. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/common/ui/YogaLayoutHelper.java +39 -0
  9. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewCallback.kt +30 -0
  10. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewEventSender.kt +80 -0
  11. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewManager.java +544 -0
  12. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventHandler.java +30 -0
  13. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventSender.java +64 -0
  14. package/android/src/main/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewManager.java +356 -0
  15. package/android/src/newarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaExpressCheckoutViewSpec.java +31 -0
  16. package/android/src/newarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaOSMViewSpec.java +31 -0
  17. package/android/src/oldarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaExpressCheckoutViewSpec.java +36 -0
  18. package/android/src/oldarch/java/com/klarna/mobile/sdk/reactnative/spec/RNKlarnaOSMViewSpec.java +32 -0
  19. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/KlarnaMobileSDKPackageTest.java +5 -1
  20. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/common/ui/YogaLayoutHelperTest.java +79 -0
  21. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/expresscheckout/KlarnaExpressCheckoutViewManagerTest.java +259 -0
  22. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventHandlerTest.java +136 -0
  23. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewEventSenderTest.java +184 -0
  24. package/android/src/test/java/com/klarna/mobile/sdk/reactnative/osm/KlarnaOSMViewManagerTest.java +385 -0
  25. package/ios/RNKlarnaMobileSDK.xcodeproj/project.pbxproj +110 -52
  26. package/ios/Sources/KlarnaExpressCheckoutViewManager.h +5 -0
  27. package/ios/Sources/KlarnaExpressCheckoutViewManager.mm +39 -0
  28. package/ios/Sources/KlarnaOSMViewManager.h +5 -0
  29. package/ios/Sources/KlarnaOSMViewManager.mm +59 -0
  30. package/ios/Sources/view/KlarnaExpressCheckoutButtonDelegateProxy.swift +145 -0
  31. package/ios/Sources/view/KlarnaExpressCheckoutViewWrapper.h +41 -0
  32. package/ios/Sources/view/KlarnaOSMViewWrapper.h +48 -0
  33. package/ios/Sources/view/newarch/KlarnaExpressCheckoutViewWrapper.mm +228 -0
  34. package/ios/Sources/view/newarch/KlarnaOSMViewWrapper.mm +294 -0
  35. package/ios/Sources/view/newarch/KlarnaStandaloneWebViewWrapper.mm +20 -13
  36. package/ios/Sources/view/oldarch/KlarnaExpressCheckoutViewWrapper.mm +222 -0
  37. package/ios/Sources/view/oldarch/KlarnaOSMViewWrapper.mm +273 -0
  38. package/ios/Sources/view/oldarch/KlarnaStandaloneWebViewWrapper.mm +11 -13
  39. package/ios/Tests/KlarnaExpressCheckoutViewWrapperTests.m +391 -0
  40. package/ios/Tests/KlarnaOSMViewWrapperTests.m +450 -0
  41. package/ios/Tests/KlarnaStandaloneWebViewWrapperTests.m +105 -0
  42. package/lib/commonjs/KlarnaCheckoutView.js +2 -4
  43. package/lib/commonjs/KlarnaCheckoutView.js.map +1 -1
  44. package/lib/commonjs/KlarnaExpressCheckoutView.js +73 -0
  45. package/lib/commonjs/KlarnaExpressCheckoutView.js.map +1 -0
  46. package/lib/commonjs/KlarnaOSMView.js +92 -0
  47. package/lib/commonjs/KlarnaOSMView.js.map +1 -0
  48. package/lib/commonjs/KlarnaPaymentView.js +32 -55
  49. package/lib/commonjs/KlarnaPaymentView.js.map +1 -1
  50. package/lib/commonjs/KlarnaSignInSDK.js +1 -1
  51. package/lib/commonjs/KlarnaSignInSDK.js.map +1 -1
  52. package/lib/commonjs/KlarnaStandaloneWebView.js +2 -4
  53. package/lib/commonjs/KlarnaStandaloneWebView.js.map +1 -1
  54. package/lib/commonjs/index.js +128 -0
  55. package/lib/commonjs/index.js.map +1 -1
  56. package/lib/commonjs/specs/KlarnaCheckoutViewNativeComponent.js +3 -5
  57. package/lib/commonjs/specs/KlarnaCheckoutViewNativeComponent.js.map +1 -1
  58. package/lib/commonjs/specs/KlarnaExpressCheckoutViewNativeComponent.js +10 -0
  59. package/lib/commonjs/specs/KlarnaExpressCheckoutViewNativeComponent.js.map +1 -0
  60. package/lib/commonjs/specs/KlarnaOSMViewNativeComponent.js +14 -0
  61. package/lib/commonjs/specs/KlarnaOSMViewNativeComponent.js.map +1 -0
  62. package/lib/commonjs/specs/KlarnaPaymentViewNativeComponent.js +3 -5
  63. package/lib/commonjs/specs/KlarnaPaymentViewNativeComponent.js.map +1 -1
  64. package/lib/commonjs/specs/KlarnaStandaloneWebViewNativeComponent.js +3 -5
  65. package/lib/commonjs/specs/KlarnaStandaloneWebViewNativeComponent.js.map +1 -1
  66. package/lib/commonjs/specs/NativeKlarnaSignIn.js +1 -2
  67. package/lib/commonjs/specs/NativeKlarnaSignIn.js.map +1 -1
  68. package/lib/commonjs/types/button/KlarnaButtonShape.js +13 -0
  69. package/lib/commonjs/types/button/KlarnaButtonShape.js.map +1 -0
  70. package/lib/commonjs/types/button/KlarnaButtonStyle.js +12 -0
  71. package/lib/commonjs/types/button/KlarnaButtonStyle.js.map +1 -0
  72. package/lib/commonjs/types/button/KlarnaButtonTheme.js +13 -0
  73. package/lib/commonjs/types/button/KlarnaButtonTheme.js.map +1 -0
  74. package/lib/commonjs/types/common/KlarnaEnvironment.js +1 -2
  75. package/lib/commonjs/types/common/KlarnaEnvironment.js.map +1 -1
  76. package/lib/commonjs/types/common/KlarnaExpressCheckoutSessionOptions.js +8 -0
  77. package/lib/commonjs/types/common/KlarnaExpressCheckoutSessionOptions.js.map +1 -0
  78. package/lib/commonjs/types/common/KlarnaMobileSDKError.js.map +1 -1
  79. package/lib/commonjs/types/common/KlarnaOSMPlacementKey.js +2 -0
  80. package/lib/commonjs/types/common/KlarnaOSMPlacementKey.js.map +1 -0
  81. package/lib/commonjs/types/common/KlarnaProductEvent.js.map +1 -1
  82. package/lib/commonjs/types/common/KlarnaRegion.js +1 -2
  83. package/lib/commonjs/types/common/KlarnaRegion.js.map +1 -1
  84. package/lib/commonjs/types/common/KlarnaTheme.js +13 -0
  85. package/lib/commonjs/types/common/KlarnaTheme.js.map +1 -0
  86. package/lib/module/KlarnaCheckoutView.js.map +1 -1
  87. package/lib/module/KlarnaExpressCheckoutView.js +64 -0
  88. package/lib/module/KlarnaExpressCheckoutView.js.map +1 -0
  89. package/lib/module/KlarnaOSMView.js +84 -0
  90. package/lib/module/KlarnaOSMView.js.map +1 -0
  91. package/lib/module/KlarnaPaymentView.js +30 -51
  92. package/lib/module/KlarnaPaymentView.js.map +1 -1
  93. package/lib/module/KlarnaSignInSDK.js.map +1 -1
  94. package/lib/module/KlarnaStandaloneWebView.js.map +1 -1
  95. package/lib/module/index.js +11 -0
  96. package/lib/module/index.js.map +1 -1
  97. package/lib/module/specs/KlarnaCheckoutViewNativeComponent.js.map +1 -1
  98. package/lib/module/specs/KlarnaExpressCheckoutViewNativeComponent.js +3 -0
  99. package/lib/module/specs/KlarnaExpressCheckoutViewNativeComponent.js.map +1 -0
  100. package/lib/module/specs/KlarnaOSMViewNativeComponent.js +7 -0
  101. package/lib/module/specs/KlarnaOSMViewNativeComponent.js.map +1 -0
  102. package/lib/module/specs/KlarnaPaymentViewNativeComponent.js.map +1 -1
  103. package/lib/module/specs/KlarnaStandaloneWebViewNativeComponent.js.map +1 -1
  104. package/lib/module/specs/NativeKlarnaSignIn.js.map +1 -1
  105. package/lib/module/types/button/KlarnaButtonShape.js +7 -0
  106. package/lib/module/types/button/KlarnaButtonShape.js.map +1 -0
  107. package/lib/module/types/button/KlarnaButtonStyle.js +6 -0
  108. package/lib/module/types/button/KlarnaButtonStyle.js.map +1 -0
  109. package/lib/module/types/button/KlarnaButtonTheme.js +7 -0
  110. package/lib/module/types/button/KlarnaButtonTheme.js.map +1 -0
  111. package/lib/module/types/common/KlarnaEnvironment.js.map +1 -1
  112. package/lib/module/types/common/KlarnaExpressCheckoutSessionOptions.js +2 -0
  113. package/lib/module/types/common/KlarnaExpressCheckoutSessionOptions.js.map +1 -0
  114. package/lib/module/types/common/KlarnaMobileSDKError.js.map +1 -1
  115. package/lib/module/types/common/KlarnaOSMPlacementKey.js +2 -0
  116. package/lib/module/types/common/KlarnaOSMPlacementKey.js.map +1 -0
  117. package/lib/module/types/common/KlarnaProductEvent.js.map +1 -1
  118. package/lib/module/types/common/KlarnaRegion.js.map +1 -1
  119. package/lib/module/types/common/KlarnaTheme.js +7 -0
  120. package/lib/module/types/common/KlarnaTheme.js.map +1 -0
  121. package/lib/typescript/KlarnaCheckoutView.d.ts.map +1 -1
  122. package/lib/typescript/KlarnaExpressCheckoutView.d.ts +47 -0
  123. package/lib/typescript/KlarnaExpressCheckoutView.d.ts.map +1 -0
  124. package/lib/typescript/KlarnaOSMView.d.ts +36 -0
  125. package/lib/typescript/KlarnaOSMView.d.ts.map +1 -0
  126. package/lib/typescript/KlarnaPaymentView.d.ts.map +1 -1
  127. package/lib/typescript/KlarnaStandaloneWebView.d.ts.map +1 -1
  128. package/lib/typescript/index.d.ts +11 -0
  129. package/lib/typescript/index.d.ts.map +1 -1
  130. package/lib/typescript/specs/KlarnaCheckoutViewNativeComponent.d.ts.map +1 -1
  131. package/lib/typescript/specs/KlarnaExpressCheckoutViewNativeComponent.d.ts +45 -0
  132. package/lib/typescript/specs/KlarnaExpressCheckoutViewNativeComponent.d.ts.map +1 -0
  133. package/lib/typescript/specs/KlarnaOSMViewNativeComponent.d.ts +35 -0
  134. package/lib/typescript/specs/KlarnaOSMViewNativeComponent.d.ts.map +1 -0
  135. package/lib/typescript/specs/KlarnaPaymentViewNativeComponent.d.ts.map +1 -1
  136. package/lib/typescript/specs/KlarnaStandaloneWebViewNativeComponent.d.ts.map +1 -1
  137. package/lib/typescript/specs/NativeKlarnaSignIn.d.ts +1 -1
  138. package/lib/typescript/specs/NativeKlarnaSignIn.d.ts.map +1 -1
  139. package/lib/typescript/types/button/KlarnaButtonShape.d.ts +6 -0
  140. package/lib/typescript/types/button/KlarnaButtonShape.d.ts.map +1 -0
  141. package/lib/typescript/types/button/KlarnaButtonStyle.d.ts +5 -0
  142. package/lib/typescript/types/button/KlarnaButtonStyle.d.ts.map +1 -0
  143. package/lib/typescript/types/button/KlarnaButtonTheme.d.ts +6 -0
  144. package/lib/typescript/types/button/KlarnaButtonTheme.d.ts.map +1 -0
  145. package/lib/typescript/types/common/KlarnaExpressCheckoutSessionOptions.d.ts +10 -0
  146. package/lib/typescript/types/common/KlarnaExpressCheckoutSessionOptions.d.ts.map +1 -0
  147. package/lib/typescript/types/common/KlarnaOSMPlacementKey.d.ts +2 -0
  148. package/lib/typescript/types/common/KlarnaOSMPlacementKey.d.ts.map +1 -0
  149. package/lib/typescript/types/common/KlarnaTheme.d.ts +6 -0
  150. package/lib/typescript/types/common/KlarnaTheme.d.ts.map +1 -0
  151. package/package.json +17 -16
  152. package/react-native-klarna-inapp-sdk.podspec +2 -1
  153. package/src/KlarnaExpressCheckoutView.tsx +169 -0
  154. package/src/KlarnaOSMView.tsx +157 -0
  155. package/src/KlarnaStandaloneWebView.tsx +1 -2
  156. package/src/index.tsx +11 -0
  157. package/src/specs/KlarnaExpressCheckoutViewNativeComponent.ts +66 -0
  158. package/src/specs/KlarnaOSMViewNativeComponent.ts +50 -0
  159. package/src/specs/NativeKlarnaSignIn.ts +1 -1
  160. package/src/types/button/KlarnaButtonShape.ts +5 -0
  161. package/src/types/button/KlarnaButtonStyle.ts +4 -0
  162. package/src/types/button/KlarnaButtonTheme.ts +5 -0
  163. package/src/types/common/KlarnaExpressCheckoutSessionOptions.ts +13 -0
  164. package/src/types/common/KlarnaOSMPlacementKey.ts +3 -0
  165. package/src/types/common/KlarnaTheme.ts +5 -0
  166. package/LICENSE +0 -201
  167. package/README.md +0 -93
@@ -0,0 +1,544 @@
1
+ package com.klarna.mobile.sdk.reactnative.expresscheckout;
2
+
3
+ import android.app.Activity;
4
+ import android.content.Context;
5
+ import android.graphics.Color;
6
+ import android.util.Log;
7
+ import android.view.Choreographer;
8
+ import android.view.View;
9
+ import android.view.ViewGroup;
10
+ import android.widget.FrameLayout;
11
+
12
+ import androidx.annotation.NonNull;
13
+ import androidx.annotation.Nullable;
14
+
15
+ import com.facebook.react.bridge.ReactContext;
16
+ import com.facebook.react.uimanager.ThemedReactContext;
17
+ import com.facebook.react.uimanager.UIManagerHelper;
18
+ import com.facebook.react.uimanager.annotations.ReactProp;
19
+ import com.facebook.react.uimanager.events.EventDispatcher;
20
+ import com.klarna.mobile.sdk.api.KlarnaEnvironment;
21
+ import com.klarna.mobile.sdk.api.KlarnaRegion;
22
+ import com.klarna.mobile.sdk.api.button.KlarnaButtonShape;
23
+ import com.klarna.mobile.sdk.api.button.KlarnaButtonStyle;
24
+ import com.klarna.mobile.sdk.api.button.KlarnaButtonTheme;
25
+ import com.klarna.mobile.sdk.api.expresscheckout.KlarnaExpressCheckoutButton;
26
+ import com.klarna.mobile.sdk.api.expresscheckout.KlarnaExpressCheckoutButtonOptions;
27
+ import com.klarna.mobile.sdk.api.expresscheckout.KlarnaExpressCheckoutButtonStyleConfiguration;
28
+ import com.klarna.mobile.sdk.api.expresscheckout.KlarnaExpressCheckoutSessionOptions;
29
+ import com.klarna.mobile.sdk.reactnative.common.ui.YogaLayoutHelper;
30
+ import com.klarna.mobile.sdk.reactnative.spec.RNKlarnaExpressCheckoutViewSpec;
31
+
32
+ import java.util.Map;
33
+ import java.util.WeakHashMap;
34
+
35
+ public class KlarnaExpressCheckoutViewManager extends RNKlarnaExpressCheckoutViewSpec<FrameLayout> {
36
+
37
+ public static final String REACT_CLASS = "RNKlarnaExpressCheckoutView";
38
+
39
+ private final Map<FrameLayout, EventDispatcher> viewToDispatcher;
40
+ private final KlarnaExpressCheckoutViewEventSender eventSender;
41
+
42
+ // Stored props per view.
43
+ private final Map<FrameLayout, ExpressCheckoutProps> viewToProps;
44
+
45
+ // Tracks whether button creation has been scheduled/completed for this view instance.
46
+ private final Map<FrameLayout, Boolean> pendingCreate;
47
+ private final Map<FrameLayout, Boolean> isButtonCreated;
48
+
49
+ // Monotonic generation per view used to invalidate stale async callbacks.
50
+ private final Map<FrameLayout, Integer> viewToGeneration;
51
+
52
+ // Stores the layout change listener per view so it can be removed on teardown.
53
+ private final Map<FrameLayout, View.OnLayoutChangeListener> viewToLayoutListener;
54
+
55
+ // Strong reference to the callback — the SDK stores it via WeakReference internally,
56
+ // so without this the callback would be garbage collected.
57
+ private final Map<FrameLayout, KlarnaExpressCheckoutViewCallback> viewToCallback;
58
+
59
+ public KlarnaExpressCheckoutViewManager() {
60
+ super();
61
+ viewToDispatcher = new WeakHashMap<>();
62
+ viewToProps = new WeakHashMap<>();
63
+ pendingCreate = new WeakHashMap<>();
64
+ isButtonCreated = new WeakHashMap<>();
65
+ viewToGeneration = new WeakHashMap<>();
66
+ viewToLayoutListener = new WeakHashMap<>();
67
+ viewToCallback = new WeakHashMap<>();
68
+ eventSender = new KlarnaExpressCheckoutViewEventSender(viewToDispatcher);
69
+ }
70
+
71
+ @NonNull
72
+ @Override
73
+ public String getName() {
74
+ return REACT_CLASS;
75
+ }
76
+
77
+ @NonNull
78
+ @Override
79
+ protected FrameLayout createViewInstance(@NonNull ThemedReactContext themedReactContext) {
80
+ FrameLayout view = new FrameLayout(themedReactContext);
81
+ view.setBackgroundColor(Color.TRANSPARENT);
82
+
83
+ EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(
84
+ (ReactContext) view.getContext(), view.getId());
85
+ viewToDispatcher.put(view, eventDispatcher);
86
+ viewToProps.put(view, new ExpressCheckoutProps());
87
+ viewToGeneration.put(view, 0);
88
+
89
+ view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
90
+ @Override
91
+ public void onViewAttachedToWindow(@NonNull View v) {
92
+ }
93
+
94
+ @Override
95
+ public void onViewDetachedFromWindow(@NonNull View v) {
96
+ tearDownButton(view);
97
+ }
98
+ });
99
+
100
+ return view;
101
+ }
102
+
103
+ @Nullable
104
+ @Override
105
+ public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
106
+ return eventSender.getExportedCustomDirectEventTypeConstants();
107
+ }
108
+
109
+ // region ReactProp setters
110
+ // Each setter stores the value in ExpressCheckoutProps and schedules (re)creation.
111
+ // The creation is deferred to the next frame so React can finish batching prop updates.
112
+
113
+ @ReactProp(name = "sessionType")
114
+ @Override
115
+ public void setSessionType(FrameLayout view, @Nullable String value) {
116
+ ExpressCheckoutProps props = viewToProps.get(view);
117
+ if (props != null) {
118
+ props.sessionType = value;
119
+ scheduleButtonCreation(view);
120
+ }
121
+ }
122
+
123
+ @ReactProp(name = "clientId")
124
+ @Override
125
+ public void setClientId(FrameLayout view, @Nullable String value) {
126
+ ExpressCheckoutProps props = viewToProps.get(view);
127
+ if (props != null) {
128
+ props.clientId = value;
129
+ scheduleButtonCreation(view);
130
+ }
131
+ }
132
+
133
+ @ReactProp(name = "clientToken")
134
+ @Override
135
+ public void setClientToken(FrameLayout view, @Nullable String value) {
136
+ ExpressCheckoutProps props = viewToProps.get(view);
137
+ if (props != null) {
138
+ props.clientToken = value;
139
+ scheduleButtonCreation(view);
140
+ }
141
+ }
142
+
143
+ @ReactProp(name = "locale")
144
+ @Override
145
+ public void setLocale(FrameLayout view, @Nullable String value) {
146
+ ExpressCheckoutProps props = viewToProps.get(view);
147
+ if (props != null) {
148
+ props.locale = value;
149
+ scheduleButtonCreation(view);
150
+ }
151
+ }
152
+
153
+ @ReactProp(name = "environment")
154
+ @Override
155
+ public void setEnvironment(FrameLayout view, @Nullable String value) {
156
+ ExpressCheckoutProps props = viewToProps.get(view);
157
+ if (props != null) {
158
+ props.environment = value;
159
+ scheduleButtonCreation(view);
160
+ }
161
+ }
162
+
163
+ @ReactProp(name = "region")
164
+ @Override
165
+ public void setRegion(FrameLayout view, @Nullable String value) {
166
+ ExpressCheckoutProps props = viewToProps.get(view);
167
+ if (props != null) {
168
+ props.region = value;
169
+ scheduleButtonCreation(view);
170
+ }
171
+ }
172
+
173
+ @ReactProp(name = "returnUrl")
174
+ @Override
175
+ public void setReturnUrl(FrameLayout view, @Nullable String value) {
176
+ ExpressCheckoutProps props = viewToProps.get(view);
177
+ if (props != null) {
178
+ props.returnUrl = value;
179
+ scheduleButtonCreation(view);
180
+ }
181
+ }
182
+
183
+ @ReactProp(name = "theme")
184
+ @Override
185
+ public void setTheme(FrameLayout view, @Nullable String value) {
186
+ ExpressCheckoutProps props = viewToProps.get(view);
187
+ if (props != null) {
188
+ props.theme = value;
189
+ scheduleButtonCreation(view);
190
+ }
191
+ }
192
+
193
+ @ReactProp(name = "shape")
194
+ @Override
195
+ public void setShape(FrameLayout view, @Nullable String value) {
196
+ ExpressCheckoutProps props = viewToProps.get(view);
197
+ if (props != null) {
198
+ props.shape = value;
199
+ scheduleButtonCreation(view);
200
+ }
201
+ }
202
+
203
+ @ReactProp(name = "buttonStyle")
204
+ @Override
205
+ public void setButtonStyle(FrameLayout view, @Nullable String value) {
206
+ ExpressCheckoutProps props = viewToProps.get(view);
207
+ if (props != null) {
208
+ props.buttonStyle = value;
209
+ scheduleButtonCreation(view);
210
+ }
211
+ }
212
+
213
+ @ReactProp(name = "autoFinalize")
214
+ @Override
215
+ public void setAutoFinalize(FrameLayout view, boolean value) {
216
+ ExpressCheckoutProps props = viewToProps.get(view);
217
+ if (props != null) {
218
+ props.autoFinalize = value;
219
+ scheduleButtonCreation(view);
220
+ }
221
+ }
222
+
223
+ @ReactProp(name = "collectShippingAddress")
224
+ @Override
225
+ public void setCollectShippingAddress(FrameLayout view, boolean value) {
226
+ ExpressCheckoutProps props = viewToProps.get(view);
227
+ if (props != null) {
228
+ props.collectShippingAddress = value;
229
+ scheduleButtonCreation(view);
230
+ }
231
+ }
232
+
233
+ @ReactProp(name = "sessionData")
234
+ @Override
235
+ public void setSessionData(FrameLayout view, @Nullable String value) {
236
+ ExpressCheckoutProps props = viewToProps.get(view);
237
+ if (props != null) {
238
+ props.sessionData = value;
239
+ scheduleButtonCreation(view);
240
+ }
241
+ }
242
+
243
+ // endregion
244
+
245
+ @Override
246
+ public void onDropViewInstance(@NonNull FrameLayout view) {
247
+ viewToDispatcher.remove(view);
248
+ viewToProps.remove(view);
249
+ pendingCreate.remove(view);
250
+ isButtonCreated.remove(view);
251
+ viewToGeneration.remove(view);
252
+ View.OnLayoutChangeListener listener = viewToLayoutListener.remove(view);
253
+ if (listener != null) {
254
+ view.removeOnLayoutChangeListener(listener);
255
+ }
256
+ viewToCallback.remove(view);
257
+ super.onDropViewInstance(view);
258
+ }
259
+
260
+ // region Button creation
261
+
262
+ /**
263
+ * Schedules button creation for the next frame so React prop batches can settle first.
264
+ * Subsequent prop changes within the same frame coalesce into a single recreate pass.
265
+ *
266
+ * <p><b>Note:</b> The KlarnaExpressCheckoutButton SDK provides no setters for individual
267
+ * properties — all configuration is passed through the constructor. Therefore every prop
268
+ * change requires a full tear-down and recreation of the button. Merchants should avoid
269
+ * changing props (e.g. theme, sessionData) once the user has tapped the button and an
270
+ * authorization flow may be in progress, as the button will be removed from the hierarchy.
271
+ * The SDK authorization UI runs in its own Activity/BottomSheet, so in practice the
272
+ * underlying React component rarely re-renders during that flow.
273
+ */
274
+ private void scheduleButtonCreation(@NonNull FrameLayout view) {
275
+ if (Boolean.TRUE.equals(pendingCreate.get(view))) {
276
+ return; // already scheduled
277
+ }
278
+ pendingCreate.put(view, Boolean.TRUE);
279
+ Choreographer.getInstance().postFrameCallback(frameTimeNanos -> {
280
+ pendingCreate.remove(view);
281
+ tearDownButton(view);
282
+ createButton(view);
283
+ });
284
+ }
285
+
286
+ private void tearDownButton(@NonNull FrameLayout view) {
287
+ // Remove the previous layout change listener to avoid stale references.
288
+ View.OnLayoutChangeListener oldListener = viewToLayoutListener.remove(view);
289
+ if (oldListener != null) {
290
+ view.removeOnLayoutChangeListener(oldListener);
291
+ }
292
+ view.removeAllViews();
293
+ isButtonCreated.remove(view);
294
+ }
295
+
296
+ private void createButton(@NonNull FrameLayout view) {
297
+ ExpressCheckoutProps props = viewToProps.get(view);
298
+ if (props == null) {
299
+ return;
300
+ }
301
+
302
+ Context context = view.getContext();
303
+ Activity activity = null;
304
+ if (context instanceof ThemedReactContext) {
305
+ activity = ((ThemedReactContext) context).getCurrentActivity();
306
+ }
307
+ Context buttonContext = activity != null ? activity : context;
308
+ String sessionData = props.sessionData != null ? props.sessionData : "";
309
+
310
+ int generation = nextGeneration(view);
311
+
312
+ try {
313
+ // Build session options based on the explicit sessionType.
314
+ KlarnaExpressCheckoutSessionOptions sessionOptions;
315
+ if ("clientToken".equals(props.sessionType)) {
316
+ sessionOptions = new KlarnaExpressCheckoutSessionOptions.ServerSideSession(
317
+ props.clientToken != null ? props.clientToken : "",
318
+ sessionData,
319
+ props.autoFinalize,
320
+ props.collectShippingAddress
321
+ );
322
+ } else {
323
+ sessionOptions = new KlarnaExpressCheckoutSessionOptions.ClientSideSession(
324
+ props.clientId != null ? props.clientId : "",
325
+ sessionData,
326
+ props.autoFinalize,
327
+ props.collectShippingAddress
328
+ );
329
+ }
330
+
331
+ // Build style configuration
332
+ KlarnaExpressCheckoutButtonStyleConfiguration styleConfig =
333
+ new KlarnaExpressCheckoutButtonStyleConfiguration(
334
+ parseTheme(props.theme),
335
+ parseShape(props.shape),
336
+ parseButtonStyle(props.buttonStyle)
337
+ );
338
+
339
+ // Build callback — store a strong reference so the SDK's WeakReference doesn't lose it.
340
+ KlarnaExpressCheckoutViewCallback callback =
341
+ new KlarnaExpressCheckoutViewCallback(view, eventSender);
342
+ viewToCallback.put(view, callback);
343
+
344
+ // Build options
345
+ KlarnaExpressCheckoutButtonOptions options = new KlarnaExpressCheckoutButtonOptions(
346
+ sessionOptions,
347
+ callback,
348
+ props.locale,
349
+ styleConfig,
350
+ parseEnvironment(props.environment),
351
+ parseRegion(props.region),
352
+ null, // KlarnaTheme
353
+ null, // resourceEndpoint
354
+ null // loggingLevel
355
+ );
356
+
357
+ KlarnaExpressCheckoutButton expressButton = new KlarnaExpressCheckoutButton(buttonContext, options);
358
+
359
+ view.addView(expressButton, new ViewGroup.LayoutParams(
360
+ ViewGroup.LayoutParams.MATCH_PARENT,
361
+ ViewGroup.LayoutParams.WRAP_CONTENT
362
+ ));
363
+
364
+ isButtonCreated.put(view, Boolean.TRUE);
365
+
366
+ // React Native's Yoga layout engine suppresses requestLayout() from child views,
367
+ // so the SDK button won't measure/layout correctly without a manual layout pass.
368
+ setupManualLayout(view, expressButton, generation);
369
+
370
+ // Send initial resize event after button renders.
371
+ // getHeight() returns pixels; React Native expects density-independent pixels (dp).
372
+ expressButton.post(() -> {
373
+ if (isCurrentGeneration(view, generation)) {
374
+ maybeSendResizeEvent(view, expressButton);
375
+ }
376
+ });
377
+ } catch (Exception e) {
378
+ Log.e("KlarnaExpressCheckout", "Failed to create express checkout button", e);
379
+ eventSender.sendKlarnaExpressCheckoutError(
380
+ view,
381
+ "ButtonCreationError",
382
+ e.getMessage() != null ? e.getMessage() : "Unknown error during button creation",
383
+ true
384
+ );
385
+ }
386
+ }
387
+
388
+ /**
389
+ * React Native's Yoga layout engine suppresses requestLayout() from native child views.
390
+ * Schedule a manual measure+layout pass every frame until the button is laid out,
391
+ * and re-layout whenever the wrapper's bounds change.
392
+ */
393
+ private void setupManualLayout(
394
+ @NonNull FrameLayout wrapper,
395
+ @NonNull KlarnaExpressCheckoutButton button,
396
+ int generation
397
+ ) {
398
+ final int[] lastHeightPx = new int[]{-1};
399
+ final int[] stableFrameCount = new int[]{0};
400
+ Choreographer.FrameCallback frameCallback = new Choreographer.FrameCallback() {
401
+ @Override
402
+ public void doFrame(long frameTimeNanos) {
403
+ if (!isCurrentGeneration(wrapper, generation) || !button.isAttachedToWindow()) {
404
+ return;
405
+ }
406
+ YogaLayoutHelper.measureAndLayout(wrapper, button);
407
+ maybeSendResizeEvent(wrapper, button);
408
+
409
+ int currentHeightPx = button.getHeight();
410
+ if (currentHeightPx <= 0) {
411
+ stableFrameCount[0] = 0;
412
+ } else if (currentHeightPx == lastHeightPx[0]) {
413
+ stableFrameCount[0] = stableFrameCount[0] + 1;
414
+ } else {
415
+ stableFrameCount[0] = 0;
416
+ }
417
+ lastHeightPx[0] = currentHeightPx;
418
+
419
+ // Keep posting while content is still unresolved or changing across frames.
420
+ if (currentHeightPx <= 0 || stableFrameCount[0] < 3) {
421
+ Choreographer.getInstance().postFrameCallback(this);
422
+ }
423
+ }
424
+ };
425
+ Choreographer.getInstance().postFrameCallback(frameCallback);
426
+
427
+ View.OnLayoutChangeListener layoutListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
428
+ if (!isCurrentGeneration(wrapper, generation) || !button.isAttachedToWindow()) {
429
+ return;
430
+ }
431
+ YogaLayoutHelper.measureAndLayout(wrapper, button);
432
+ maybeSendResizeEvent(wrapper, button);
433
+ };
434
+ viewToLayoutListener.put(wrapper, layoutListener);
435
+ wrapper.addOnLayoutChangeListener(layoutListener);
436
+ }
437
+
438
+ private int nextGeneration(@NonNull FrameLayout view) {
439
+ int next = (viewToGeneration.get(view) == null ? 0 : viewToGeneration.get(view)) + 1;
440
+ viewToGeneration.put(view, next);
441
+ return next;
442
+ }
443
+
444
+ private boolean isCurrentGeneration(@NonNull FrameLayout view, int generation) {
445
+ Integer current = viewToGeneration.get(view);
446
+ return current != null && current == generation;
447
+ }
448
+
449
+ private void maybeSendResizeEvent(
450
+ @NonNull FrameLayout wrapper,
451
+ @NonNull View button
452
+ ) {
453
+ if (button.getHeight() <= 0) {
454
+ return;
455
+ }
456
+ float density = wrapper.getResources().getDisplayMetrics().density;
457
+ int heightInDp = Math.round(button.getHeight() / density);
458
+ ExpressCheckoutProps props = viewToProps.get(wrapper);
459
+ if (props == null || props.lastSentHeightDp == heightInDp) {
460
+ return;
461
+ }
462
+ props.lastSentHeightDp = heightInDp;
463
+ eventSender.sendOnResizedEvent(wrapper, heightInDp);
464
+ }
465
+
466
+ // endregion
467
+
468
+ // region Prop parsing helpers
469
+
470
+ @Nullable
471
+ static KlarnaButtonTheme parseTheme(@Nullable String value) {
472
+ if (value == null) return null;
473
+ switch (value) {
474
+ case "light": return KlarnaButtonTheme.LIGHT;
475
+ case "dark": return KlarnaButtonTheme.DARK;
476
+ case "auto": return KlarnaButtonTheme.AUTO;
477
+ default: return null;
478
+ }
479
+ }
480
+
481
+ @Nullable
482
+ static KlarnaButtonShape parseShape(@Nullable String value) {
483
+ if (value == null) return null;
484
+ switch (value) {
485
+ case "roundedRect": return KlarnaButtonShape.ROUNDED_RECT;
486
+ case "pill": return KlarnaButtonShape.PILL;
487
+ case "rectangle": return KlarnaButtonShape.RECTANGLE;
488
+ default: return null;
489
+ }
490
+ }
491
+
492
+ @Nullable
493
+ static KlarnaButtonStyle parseButtonStyle(@Nullable String value) {
494
+ if (value == null) return null;
495
+ switch (value) {
496
+ case "filled": return KlarnaButtonStyle.FILLED;
497
+ case "outlined": return KlarnaButtonStyle.OUTLINED;
498
+ default: return null;
499
+ }
500
+ }
501
+
502
+ @Nullable
503
+ static KlarnaEnvironment parseEnvironment(@Nullable String value) {
504
+ if (value == null) return null;
505
+ switch (value) {
506
+ case "playground": return KlarnaEnvironment.PLAYGROUND;
507
+ case "production": return KlarnaEnvironment.PRODUCTION;
508
+ default: return null;
509
+ }
510
+ }
511
+
512
+ @Nullable
513
+ static KlarnaRegion parseRegion(@Nullable String value) {
514
+ if (value == null) return null;
515
+ switch (value) {
516
+ case "eu": return KlarnaRegion.EU;
517
+ case "na": return KlarnaRegion.NA;
518
+ case "oc": return KlarnaRegion.OC;
519
+ default: return null;
520
+ }
521
+ }
522
+
523
+ // endregion
524
+
525
+ /**
526
+ * Holds the current prop values for a single view instance.
527
+ */
528
+ static class ExpressCheckoutProps {
529
+ String sessionType;
530
+ String clientId;
531
+ String clientToken;
532
+ String locale;
533
+ String environment;
534
+ String region;
535
+ String returnUrl;
536
+ String theme;
537
+ String shape;
538
+ String buttonStyle;
539
+ boolean autoFinalize = true;
540
+ boolean collectShippingAddress = false;
541
+ String sessionData;
542
+ int lastSentHeightDp = -1;
543
+ }
544
+ }
@@ -0,0 +1,30 @@
1
+ package com.klarna.mobile.sdk.reactnative.osm;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.klarna.mobile.sdk.KlarnaMobileSDKError;
6
+ import com.klarna.mobile.sdk.api.KlarnaEventHandler;
7
+ import com.klarna.mobile.sdk.api.KlarnaProductEvent;
8
+ import com.klarna.mobile.sdk.api.component.KlarnaComponent;
9
+ import com.klarna.mobile.sdk.api.osm.KlarnaOSMView;
10
+
11
+ public class KlarnaOSMViewEventHandler implements KlarnaEventHandler {
12
+
13
+ private final KlarnaOSMViewEventSender eventSender;
14
+
15
+ public KlarnaOSMViewEventHandler(@NonNull KlarnaOSMViewEventSender eventSender) {
16
+ this.eventSender = eventSender;
17
+ }
18
+
19
+ @Override
20
+ public void onEvent(@NonNull KlarnaComponent klarnaComponent, @NonNull KlarnaProductEvent klarnaProductEvent) {
21
+ // OSM view does not emit product events
22
+ }
23
+
24
+ @Override
25
+ public void onError(@NonNull KlarnaComponent klarnaComponent, @NonNull KlarnaMobileSDKError klarnaMobileSDKError) {
26
+ if (klarnaComponent instanceof KlarnaOSMView) {
27
+ eventSender.sendKlarnaMobileSDKError((KlarnaOSMView) klarnaComponent, klarnaMobileSDKError);
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,64 @@
1
+ package com.klarna.mobile.sdk.reactnative.osm;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+
6
+ import com.facebook.react.bridge.WritableMap;
7
+ import com.facebook.react.uimanager.events.EventDispatcher;
8
+ import com.klarna.mobile.sdk.KlarnaMobileSDKError;
9
+ import com.klarna.mobile.sdk.api.osm.KlarnaOSMView;
10
+ import com.klarna.mobile.sdk.reactnative.common.event.ComponentEventSender;
11
+ import com.klarna.mobile.sdk.reactnative.common.ui.WrapperView;
12
+ import com.klarna.mobile.sdk.reactnative.common.util.ArgumentsUtil;
13
+
14
+ import java.util.Arrays;
15
+ import java.util.Collection;
16
+ import java.util.HashMap;
17
+ import java.util.Map;
18
+
19
+ public class KlarnaOSMViewEventSender extends ComponentEventSender<WrapperView<KlarnaOSMView>> {
20
+
21
+ private static final String EVENT_NAME_ON_ERROR = "onError";
22
+ private static final String EVENT_NAME_ON_RESIZED = "onResized";
23
+ private static final String EVENT_NAME_ON_OSM_VIEW_READY = "onOSMViewReady";
24
+
25
+ KlarnaOSMViewEventSender(@NonNull final Map<WrapperView<KlarnaOSMView>, EventDispatcher> viewToDispatcher) {
26
+ super(viewToDispatcher);
27
+ }
28
+
29
+ @Override
30
+ public Collection<String> getCallbackEventNames() {
31
+ return Arrays.asList(
32
+ EVENT_NAME_ON_ERROR,
33
+ EVENT_NAME_ON_RESIZED,
34
+ EVENT_NAME_ON_OSM_VIEW_READY
35
+ );
36
+ }
37
+
38
+ public void sendKlarnaMobileSDKError(@Nullable KlarnaOSMView view, @NonNull KlarnaMobileSDKError klarnaMobileSDKError) {
39
+ sendError(view, klarnaMobileSDKError.getName(), klarnaMobileSDKError.getMessage(), klarnaMobileSDKError.isFatal());
40
+ }
41
+
42
+ public void sendError(@Nullable KlarnaOSMView view, @NonNull String name, @NonNull String message, boolean isFatal) {
43
+ WritableMap errorMap = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
44
+ put("name", name);
45
+ put("message", message);
46
+ put("isFatal", isFatal);
47
+ }});
48
+ WritableMap params = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
49
+ put("error", errorMap);
50
+ }});
51
+ postEventForView(view, EVENT_NAME_ON_ERROR, params);
52
+ }
53
+
54
+ public void sendOnResizedEvent(@Nullable KlarnaOSMView view, int height) {
55
+ WritableMap params = ArgumentsUtil.createMap(new HashMap<String, Object>() {{
56
+ put("height", String.valueOf(height));
57
+ }});
58
+ postEventForView(view, EVENT_NAME_ON_RESIZED, params);
59
+ }
60
+
61
+ public void sendOnOSMViewReadyEvent(@Nullable KlarnaOSMView view) {
62
+ postEventForView(view, EVENT_NAME_ON_OSM_VIEW_READY, null);
63
+ }
64
+ }