@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.
- package/README.md +80 -28
- package/android/build.gradle +97 -0
- package/android/src/main/AndroidManifest.xml +1 -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,76 @@
|
|
|
1
|
+
package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.JavaOnlyMap
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertNotNull
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Before
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
import org.mockito.MockedStatic
|
|
12
|
+
import org.mockito.Mockito
|
|
13
|
+
|
|
14
|
+
class KlarnaMessagingPlacementViewManagerTest {
|
|
15
|
+
|
|
16
|
+
private lateinit var manager: KlarnaMessagingPlacementViewManager
|
|
17
|
+
private lateinit var mockedArguments: MockedStatic<Arguments>
|
|
18
|
+
|
|
19
|
+
@Before
|
|
20
|
+
fun setUp() {
|
|
21
|
+
manager = KlarnaMessagingPlacementViewManager()
|
|
22
|
+
mockedArguments = Mockito.mockStatic(Arguments::class.java)
|
|
23
|
+
mockedArguments.`when`<JavaOnlyMap> { Arguments.createMap() }
|
|
24
|
+
.thenAnswer { JavaOnlyMap() }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@After
|
|
28
|
+
fun tearDown() {
|
|
29
|
+
mockedArguments.close()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// MARK: - getName
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun getName_returnsCorrectReactClass() {
|
|
36
|
+
assertEquals("RNKlarnaMessagingPlacementView", manager.name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MARK: - getExportedCustomDirectEventTypeConstants
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
fun eventConstants_containsOnError() {
|
|
43
|
+
val constants = manager.exportedCustomDirectEventTypeConstants
|
|
44
|
+
assertNotNull(constants)
|
|
45
|
+
assertTrue(constants!!.containsKey("onError"))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun eventConstants_containsOnResized() {
|
|
50
|
+
val constants = manager.exportedCustomDirectEventTypeConstants
|
|
51
|
+
assertNotNull(constants)
|
|
52
|
+
assertTrue(constants!!.containsKey("onResized"))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun eventConstants_onErrorRegistrationName() {
|
|
57
|
+
val constants = manager.exportedCustomDirectEventTypeConstants!!
|
|
58
|
+
@Suppress("UNCHECKED_CAST")
|
|
59
|
+
val onError = constants["onError"] as Map<String, Any>
|
|
60
|
+
assertEquals("onError", onError["registrationName"])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun eventConstants_onResizedRegistrationName() {
|
|
65
|
+
val constants = manager.exportedCustomDirectEventTypeConstants!!
|
|
66
|
+
@Suppress("UNCHECKED_CAST")
|
|
67
|
+
val onResized = constants["onResized"] as Map<String, Any>
|
|
68
|
+
assertEquals("onResized", onResized["registrationName"])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun eventConstants_hasExactlyTwoEntries() {
|
|
73
|
+
val constants = manager.exportedCustomDirectEventTypeConstants!!
|
|
74
|
+
assertEquals(2, constants.size)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
|
|
2
|
+
|
|
3
|
+
import com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging.KlarnaMessagingViewManagerHelpers.buildConfiguration
|
|
4
|
+
import com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging.KlarnaMessagingViewManagerHelpers.parseAmount
|
|
5
|
+
import com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging.KlarnaMessagingViewManagerHelpers.parseTheme
|
|
6
|
+
import com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging.KlarnaMessagingViewManagerHelpers.resolvePlacementType
|
|
7
|
+
import com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging.KlarnaMessagingViewManagerHelpers.shouldReportHeight
|
|
8
|
+
import com.klarna.mobile.sdk.api.KlarnaTheme
|
|
9
|
+
import com.klarna.mobile.sdk.klarna.network.messaging.api.KlarnaMessagingPlacementConfiguration
|
|
10
|
+
import org.junit.Assert.assertEquals
|
|
11
|
+
import org.junit.Assert.assertFalse
|
|
12
|
+
import org.junit.Assert.assertNull
|
|
13
|
+
import org.junit.Assert.assertTrue
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
|
|
16
|
+
class KlarnaMessagingViewManagerHelpersTest {
|
|
17
|
+
|
|
18
|
+
// MARK: - parseTheme
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun parseTheme_light() {
|
|
22
|
+
assertEquals(KlarnaTheme.LIGHT, parseTheme("light"))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun parseTheme_dark() {
|
|
27
|
+
assertEquals(KlarnaTheme.DARK, parseTheme("dark"))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun parseTheme_automatic() {
|
|
32
|
+
assertEquals(KlarnaTheme.AUTOMATIC, parseTheme("automatic"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun parseTheme_nullInput_returnsNull() {
|
|
37
|
+
assertNull(parseTheme(null))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun parseTheme_unknownValue_returnsNull() {
|
|
42
|
+
assertNull(parseTheme("neon"))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun parseTheme_isCaseSensitive() {
|
|
47
|
+
// Contract is lowercase only; uppercase should not match.
|
|
48
|
+
assertNull(parseTheme("LIGHT"))
|
|
49
|
+
assertNull(parseTheme("Light"))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun parseTheme_emptyString_returnsNull() {
|
|
54
|
+
assertNull(parseTheme(""))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// MARK: - parseAmount
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun parseAmount_validInteger() {
|
|
61
|
+
assertEquals(12345L, parseAmount("12345"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
fun parseAmount_zero() {
|
|
66
|
+
assertEquals(0L, parseAmount("0"))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
fun parseAmount_negative() {
|
|
71
|
+
assertEquals(-500L, parseAmount("-500"))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
fun parseAmount_null_returnsNull() {
|
|
76
|
+
assertNull(parseAmount(null))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun parseAmount_empty_returnsNull() {
|
|
81
|
+
assertNull(parseAmount(""))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun parseAmount_nonNumeric_returnsNull() {
|
|
86
|
+
assertNull(parseAmount("abc"))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
fun parseAmount_fractional_returnsNull() {
|
|
91
|
+
// Amounts are minor units (integers). Decimals must be rejected.
|
|
92
|
+
assertNull(parseAmount("12.5"))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun parseAmount_overflow_returnsNull() {
|
|
97
|
+
// Larger than Long.MAX_VALUE.
|
|
98
|
+
assertNull(parseAmount("99999999999999999999"))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - resolvePlacementType
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
fun resolvePlacementType_badge_returnsBadge() {
|
|
105
|
+
assertEquals(PlacementType.BADGE, resolvePlacementType("CreditPromotionBadge"))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test
|
|
109
|
+
fun resolvePlacementType_autoSize_returnsAutoSize() {
|
|
110
|
+
assertEquals(PlacementType.AUTO_SIZE, resolvePlacementType("CreditPromotionAutoSize"))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun resolvePlacementType_null_defaultsToAutoSize() {
|
|
115
|
+
assertEquals(PlacementType.AUTO_SIZE, resolvePlacementType(null))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun resolvePlacementType_unknown_defaultsToAutoSize() {
|
|
120
|
+
assertEquals(PlacementType.AUTO_SIZE, resolvePlacementType("SomethingElse"))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Test
|
|
124
|
+
fun resolvePlacementType_caseSensitive() {
|
|
125
|
+
// Only exact "CreditPromotionBadge" returns badge.
|
|
126
|
+
assertEquals(PlacementType.AUTO_SIZE, resolvePlacementType("creditpromotionbadge"))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// MARK: - buildConfiguration
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
fun buildConfiguration_badge_returnsBadgeConfiguration() {
|
|
133
|
+
val config = buildConfiguration("CreditPromotionBadge", KlarnaTheme.LIGHT, 1000L, "USD")
|
|
134
|
+
assertTrue(
|
|
135
|
+
"Expected CreditPromotionBadge but got ${config::class.simpleName}",
|
|
136
|
+
config is KlarnaMessagingPlacementConfiguration.CreditPromotionBadge
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun buildConfiguration_autoSize_returnsAutoSizeConfiguration() {
|
|
142
|
+
val config = buildConfiguration("CreditPromotionAutoSize", KlarnaTheme.DARK, 2000L, "EUR")
|
|
143
|
+
assertTrue(
|
|
144
|
+
"Expected CreditPromotionAutoSize but got ${config::class.simpleName}",
|
|
145
|
+
config is KlarnaMessagingPlacementConfiguration.CreditPromotionAutoSize
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test
|
|
150
|
+
fun buildConfiguration_null_defaultsToAutoSize() {
|
|
151
|
+
val config = buildConfiguration(null, null, 100L, "SEK")
|
|
152
|
+
assertTrue(config is KlarnaMessagingPlacementConfiguration.CreditPromotionAutoSize)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun buildConfiguration_unknown_defaultsToAutoSize() {
|
|
157
|
+
val config = buildConfiguration("UnknownType", null, 100L, "SEK")
|
|
158
|
+
assertTrue(config is KlarnaMessagingPlacementConfiguration.CreditPromotionAutoSize)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test
|
|
162
|
+
fun buildConfiguration_passesNullThroughWhenThemeIsNull() {
|
|
163
|
+
// Should not throw when theme is null.
|
|
164
|
+
val config = buildConfiguration("CreditPromotionBadge", null, 100L, "GBP")
|
|
165
|
+
assertTrue(config is KlarnaMessagingPlacementConfiguration.CreditPromotionBadge)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MARK: - shouldReportHeight
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
fun shouldReportHeight_firstReport_isReported() {
|
|
172
|
+
assertTrue(shouldReportHeight(null, 100))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@Test
|
|
176
|
+
fun shouldReportHeight_changedHeight_isReported() {
|
|
177
|
+
assertTrue(shouldReportHeight(100, 150))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@Test
|
|
181
|
+
fun shouldReportHeight_sameHeight_isSkipped() {
|
|
182
|
+
assertFalse(shouldReportHeight(100, 100))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
fun shouldReportHeight_zero_isSkipped() {
|
|
187
|
+
assertFalse(shouldReportHeight(null, 0))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Test
|
|
191
|
+
fun shouldReportHeight_negative_isSkipped() {
|
|
192
|
+
assertFalse(shouldReportHeight(null, -10))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Test
|
|
196
|
+
fun shouldReportHeight_zeroAfterValidHeight_isSkipped() {
|
|
197
|
+
// A spurious 0 report must not clobber a previously reported height.
|
|
198
|
+
assertFalse(shouldReportHeight(100, 0))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import KlarnaNetworkMessaging
|
|
4
|
+
import KlarnaNetworkCore
|
|
5
|
+
import KlarnaCore
|
|
6
|
+
@_spi(RNKlarnaNetworkCore) import react_native_klarna_network_core
|
|
7
|
+
|
|
8
|
+
enum PlacementType: String {
|
|
9
|
+
case autoSize = "CreditPromotionAutoSize"
|
|
10
|
+
case badge = "CreditPromotionBadge"
|
|
11
|
+
|
|
12
|
+
init(rawString: String?) {
|
|
13
|
+
self = PlacementType(rawValue: rawString ?? "") ?? .autoSize
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@objc(KlarnaMessagingPlacementViewFactory)
|
|
18
|
+
public class KlarnaMessagingPlacementViewFactory: NSObject {
|
|
19
|
+
|
|
20
|
+
@objc public static let defaultPlacementType = PlacementType.autoSize.rawValue
|
|
21
|
+
|
|
22
|
+
@objc public static func resolveKlarna(instanceId: String) -> AnyObject? {
|
|
23
|
+
return KlarnaNetworkCoreModuleImpl.getInstance(instanceId: instanceId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static func parseTheme(_ value: String) -> KlarnaTheme? {
|
|
27
|
+
switch value {
|
|
28
|
+
case "light": return .light
|
|
29
|
+
case "dark": return .dark
|
|
30
|
+
case "automatic": return .automatic
|
|
31
|
+
default: return nil
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static func makeConfiguration(
|
|
36
|
+
placementType: String,
|
|
37
|
+
theme: KlarnaTheme?,
|
|
38
|
+
amount: Int64,
|
|
39
|
+
currency: String
|
|
40
|
+
) -> KlarnaMessagingPlacementConfiguration {
|
|
41
|
+
let intAmount = Int(amount)
|
|
42
|
+
switch PlacementType(rawString: placementType) {
|
|
43
|
+
case .badge:
|
|
44
|
+
return .creditPromotionBadge(amount: intAmount, currency: currency, theme: theme)
|
|
45
|
+
case .autoSize:
|
|
46
|
+
return .creditPromotionAutoSize(amount: intAmount, currency: currency, theme: theme)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@objc public static func createPlacementView(
|
|
51
|
+
klarna: AnyObject,
|
|
52
|
+
placementType: String,
|
|
53
|
+
theme: String,
|
|
54
|
+
amount: Int64,
|
|
55
|
+
currency: String
|
|
56
|
+
) -> UIView? {
|
|
57
|
+
guard let klarnaInstance = klarna as? Klarna else {
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let parsedTheme = parseTheme(theme)
|
|
62
|
+
let configuration = makeConfiguration(
|
|
63
|
+
placementType: placementType,
|
|
64
|
+
theme: parsedTheme,
|
|
65
|
+
amount: amount,
|
|
66
|
+
currency: currency
|
|
67
|
+
)
|
|
68
|
+
let view = KlarnaMessagingPlacementView(klarna: klarnaInstance, configuration: configuration)
|
|
69
|
+
// Align the trait collection with the explicit theme so the SDK's
|
|
70
|
+
// trait-driven colors don't conflict with theme-driven colors when the
|
|
71
|
+
// JS prop opposes the phone's system mode (e.g. theme=dark on a light phone).
|
|
72
|
+
switch parsedTheme {
|
|
73
|
+
case .light:
|
|
74
|
+
view.overrideUserInterfaceStyle = .light
|
|
75
|
+
case .dark:
|
|
76
|
+
view.overrideUserInterfaceStyle = .dark
|
|
77
|
+
default:
|
|
78
|
+
view.overrideUserInterfaceStyle = .unspecified
|
|
79
|
+
}
|
|
80
|
+
// Match Android: render the SDK placement on a transparent surface so the
|
|
81
|
+
// host app's background shows through. The wrapper re-applies this on every
|
|
82
|
+
// layout pass (see clearBackgrounds:) because the SDK repaints the root and
|
|
83
|
+
// its UITextView with theme-derived colors after construction.
|
|
84
|
+
view.backgroundColor = .clear
|
|
85
|
+
return view
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Sets `backgroundColor = .clear` on the SDK root and on any UITextView in its
|
|
89
|
+
/// subtree so the host app's background shows through. Called from the Fabric
|
|
90
|
+
/// wrapper's `layoutSubviews` to survive SDK repaints (the SDK resets the root
|
|
91
|
+
/// to its theme background and the UITextView to `systemBackgroundColor`).
|
|
92
|
+
@objc public static func clearBackgrounds(_ view: UIView) {
|
|
93
|
+
view.backgroundColor = .clear
|
|
94
|
+
for subview in view.subviews {
|
|
95
|
+
if subview is UITextView {
|
|
96
|
+
subview.backgroundColor = .clear
|
|
97
|
+
}
|
|
98
|
+
clearBackgrounds(subview)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
@objc public protocol KlarnaMessagingHeightDelegate: AnyObject {
|
|
5
|
+
func placementViewDidUpdateHeight(_ height: CGFloat)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@objc(KlarnaMessagingPlacementViewHelper)
|
|
9
|
+
public class KlarnaMessagingPlacementViewHelper: NSObject {
|
|
10
|
+
|
|
11
|
+
// MARK: - Amount Parsing
|
|
12
|
+
|
|
13
|
+
/// Strict integer-only parser that mirrors Android's `String.toLongOrNull()`.
|
|
14
|
+
/// Returns `nil` for empty, non-integer, or out-of-range inputs instead of
|
|
15
|
+
/// falling back to `NSString.longLongValue`, which silently coerces `"abc"` /
|
|
16
|
+
/// `"12.5"` to `0`.
|
|
17
|
+
@objc public static func parseAmount(_ value: String?) -> NSNumber? {
|
|
18
|
+
guard let value = value, !value.isEmpty, let parsed = Int64(value) else {
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
return NSNumber(value: parsed)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// MARK: - Height Calculation
|
|
25
|
+
|
|
26
|
+
/// Determines the natural content height of `view` using a three-tier fallback:
|
|
27
|
+
/// 1. Auto Layout `systemLayoutSizeFitting` constrained to `containerWidth`.
|
|
28
|
+
/// 2. The view's current `frame.size.height` (for views laid out manually).
|
|
29
|
+
/// 3. The bottommost subview's `maxY` (last-resort introspection).
|
|
30
|
+
@objc public static func calculateHeight(for view: UIView, containerWidth: CGFloat) -> CGFloat {
|
|
31
|
+
view.setNeedsLayout()
|
|
32
|
+
view.layoutIfNeeded()
|
|
33
|
+
|
|
34
|
+
let fittingWidth = containerWidth > 0 ? containerWidth : UIScreen.main.bounds.width
|
|
35
|
+
let fittingSize = view.systemLayoutSizeFitting(
|
|
36
|
+
CGSize(width: fittingWidth, height: UIView.layoutFittingCompressedSize.height),
|
|
37
|
+
withHorizontalFittingPriority: .required,
|
|
38
|
+
verticalFittingPriority: .fittingSizeLevel
|
|
39
|
+
)
|
|
40
|
+
if fittingSize.height > 0 {
|
|
41
|
+
return fittingSize.height
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if view.frame.size.height > 0 {
|
|
45
|
+
return view.frame.size.height
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var maxBottom: CGFloat = 0
|
|
49
|
+
for subview in view.subviews {
|
|
50
|
+
let bottom = subview.frame.maxY
|
|
51
|
+
if bottom > maxBottom {
|
|
52
|
+
maxBottom = bottom
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return maxBottom
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Subview Setup
|
|
59
|
+
|
|
60
|
+
/// Adds `placementView` to `container` pinned to top/leading/trailing edges.
|
|
61
|
+
/// When `pinWidth` is `true` an explicit width constraint equal to
|
|
62
|
+
/// `container.bounds.width` is also added (no-op if the container has zero
|
|
63
|
+
/// width). The Fabric wrapper currently always passes `false` and lets Auto
|
|
64
|
+
/// Layout drive width via the trailing constraint.
|
|
65
|
+
@objc public static func addPlacementSubview(_ placementView: UIView, to container: UIView, pinWidth: Bool) {
|
|
66
|
+
placementView.translatesAutoresizingMaskIntoConstraints = false
|
|
67
|
+
container.addSubview(placementView)
|
|
68
|
+
|
|
69
|
+
NSLayoutConstraint.activate([
|
|
70
|
+
placementView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
71
|
+
placementView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
72
|
+
placementView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
if pinWidth {
|
|
76
|
+
let width = container.bounds.size.width
|
|
77
|
+
if width > 0 {
|
|
78
|
+
NSLayoutConstraint.activate([
|
|
79
|
+
placementView.widthAnchor.constraint(equalToConstant: width),
|
|
80
|
+
])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Height Observer
|
|
87
|
+
|
|
88
|
+
/// Observes a placement view's `bounds` via KVO and fires staggered delayed checks
|
|
89
|
+
/// to catch asynchronous content loads. Reports distinct height changes to its `delegate`.
|
|
90
|
+
@objc(KlarnaMessagingHeightObserver)
|
|
91
|
+
public class KlarnaMessagingHeightObserver: NSObject {
|
|
92
|
+
|
|
93
|
+
@objc public weak var delegate: KlarnaMessagingHeightDelegate?
|
|
94
|
+
|
|
95
|
+
/// Held strongly while observation is active so `removeObserver` always sees a
|
|
96
|
+
/// live target (KVO crashes if the observed object deallocates first).
|
|
97
|
+
/// Cleared in `stopObserving` after the observer is removed.
|
|
98
|
+
private var observedView: UIView?
|
|
99
|
+
private var isObserving = false
|
|
100
|
+
private var lastReportedHeight: CGFloat = 0
|
|
101
|
+
private var pendingDelayedChecks: Int = 0
|
|
102
|
+
private let delayIntervals: [Double]
|
|
103
|
+
|
|
104
|
+
@objc public init(delayIntervals: [Double]) {
|
|
105
|
+
self.delayIntervals = delayIntervals
|
|
106
|
+
super.init()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Starts observing `view` for height changes using KVO on `bounds` plus
|
|
110
|
+
/// staggered delayed checks (configured via `delayIntervals`).
|
|
111
|
+
@objc public func observe(_ view: UIView) {
|
|
112
|
+
stopObserving()
|
|
113
|
+
observedView = view
|
|
114
|
+
lastReportedHeight = 0
|
|
115
|
+
pendingDelayedChecks = delayIntervals.count
|
|
116
|
+
isObserving = true
|
|
117
|
+
view.addObserver(self, forKeyPath: "bounds", options: .new, context: nil)
|
|
118
|
+
|
|
119
|
+
for delay in delayIntervals {
|
|
120
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
121
|
+
guard let self = self else { return }
|
|
122
|
+
self.checkAndReportHeight()
|
|
123
|
+
self.pendingDelayedChecks -= 1
|
|
124
|
+
if self.pendingDelayedChecks <= 0 && self.lastReportedHeight == 0 {
|
|
125
|
+
self.delegate?.placementViewDidUpdateHeight(0)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@objc public func stopObserving() {
|
|
132
|
+
if isObserving, let view = observedView {
|
|
133
|
+
view.removeObserver(self, forKeyPath: "bounds")
|
|
134
|
+
}
|
|
135
|
+
isObserving = false
|
|
136
|
+
observedView = nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Measures the observed view's content height and notifies the delegate if
|
|
140
|
+
/// the value changed since the last report (deduplicates identical heights).
|
|
141
|
+
@objc public func checkAndReportHeight() {
|
|
142
|
+
guard let view = observedView else { return }
|
|
143
|
+
|
|
144
|
+
let containerWidth = view.superview?.bounds.size.width ?? 0
|
|
145
|
+
let height = KlarnaMessagingPlacementViewHelper.calculateHeight(for: view, containerWidth: containerWidth)
|
|
146
|
+
|
|
147
|
+
if height > 0 && height != lastReportedHeight {
|
|
148
|
+
lastReportedHeight = height
|
|
149
|
+
delegate?.placementViewDidUpdateHeight(height)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - KVO
|
|
154
|
+
|
|
155
|
+
public override func observeValue(
|
|
156
|
+
forKeyPath keyPath: String?,
|
|
157
|
+
of object: Any?,
|
|
158
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
159
|
+
context: UnsafeMutableRawPointer?
|
|
160
|
+
) {
|
|
161
|
+
if keyPath == "bounds", (object as? UIView) === observedView {
|
|
162
|
+
DispatchQueue.main.async { [weak self] in
|
|
163
|
+
self?.checkAndReportHeight()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
deinit {
|
|
169
|
+
stopObserving()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#import <React/RCTUIManager.h>
|
|
2
|
+
#import <React/RCTLog.h>
|
|
3
|
+
#import "KlarnaMessagingPlacementViewManager.h"
|
|
4
|
+
#import "view/KlarnaMessagingPlacementViewWrapper.h"
|
|
5
|
+
|
|
6
|
+
@implementation KlarnaMessagingPlacementViewManager
|
|
7
|
+
|
|
8
|
+
+ (void)initialize
|
|
9
|
+
{
|
|
10
|
+
if (self != [KlarnaMessagingPlacementViewManager class]) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
#ifndef RCT_NEW_ARCH_ENABLED
|
|
14
|
+
RCTLogError(@"KlarnaMessagingPlacementViewManager: react-native-klarna-network-messaging does not support the old architecture. Please enable the New Architecture (RCT_NEW_ARCH_ENABLED=1) before running pod install.");
|
|
15
|
+
#endif
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
RCT_EXPORT_MODULE(RNKlarnaMessagingPlacementView)
|
|
19
|
+
|
|
20
|
+
#pragma mark - View Properties
|
|
21
|
+
|
|
22
|
+
RCT_EXPORT_VIEW_PROPERTY(placementType, NSString)
|
|
23
|
+
RCT_EXPORT_VIEW_PROPERTY(theme, NSString)
|
|
24
|
+
RCT_EXPORT_VIEW_PROPERTY(amount, NSString)
|
|
25
|
+
RCT_EXPORT_VIEW_PROPERTY(currency, NSString)
|
|
26
|
+
RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock)
|
|
27
|
+
RCT_EXPORT_VIEW_PROPERTY(onResized, RCTDirectEventBlock)
|
|
28
|
+
|
|
29
|
+
#pragma mark - View Creation
|
|
30
|
+
|
|
31
|
+
- (UIView *)view
|
|
32
|
+
{
|
|
33
|
+
KlarnaMessagingPlacementViewWrapper *wrapper = [KlarnaMessagingPlacementViewWrapper new];
|
|
34
|
+
return wrapper;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@end
|