@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
package/README.md CHANGED
@@ -1,45 +1,97 @@
1
1
  # @klarna/react-native-klarna-network-messaging
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ [![NPM][npm-badge]][npm-url]
4
+ [![React Native][dependency-badge]][dependency-url]
5
+ [![Platform][platform-badge]][platform-url]
6
+ [![License][license-badge]][license-url]
7
+ [![Developed at Klarna][klarna-badge]][klarna-url]
4
8
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
9
+ React Native bindings for Klarna **messaging placements**: a Fabric-native `KlarnaMessagingPlacementView` that renders on-device messaging (for example credit promotion placements) with **amount**, **currency**, and **placement type**, an optional **theme**, automatic height from native resize events, and **error** callbacks.
6
10
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
11
+ This package builds on [`@klarna/react-native-klarna-network-core`](https://www.npmjs.com/package/@klarna/react-native-klarna-network-core): initialize `Klarna` from core before showing placements; this module re-exports `KlarnaTheme` for convenience.
8
12
 
9
- ## Purpose
13
+ ## Requirements
10
14
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@klarna/react-native-klarna-network-messaging`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
+ - iOS 13 or later.
16
+ - Android 5.0 (API 21) or later.
17
+ - React Native 0.76.0 or later.
18
+ - React Native New Architecture enabled.
15
19
 
16
- ## What is OIDC Trusted Publishing?
20
+ ### NPM
17
21
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
22
+ ```shell
23
+ npm install @klarna/react-native-klarna-network-messaging --save
24
+ ```
19
25
 
20
- ## Setup Instructions
26
+ ### Yarn
21
27
 
22
- To properly configure OIDC trusted publishing for this package:
28
+ ```shell
29
+ yarn add @klarna/react-native-klarna-network-messaging
30
+ ```
23
31
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
32
+ You also need [`@klarna/react-native-klarna-network-core`](https://www.npmjs.com/package/@klarna/react-native-klarna-network-core) (peer dependency) for initialization and shared types.
28
33
 
29
- ## DO NOT USE THIS PACKAGE
34
+ ## Usage
30
35
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
+ Initialize a `Klarna` instance from core (see the [core package README](https://github.com/klarna/react-native-klarna-inapp-sdk/blob/master/packages/klarna-network-core/README.md)) and pass it to the placement view along with its configuration:
36
37
 
37
- ## More Information
38
+ ```tsx
39
+ import {
40
+ KlarnaMessagingPlacementView,
41
+ KlarnaMessagingPlacementType,
42
+ KlarnaTheme,
43
+ } from '@klarna/react-native-klarna-network-messaging';
44
+ import type { Klarna } from '@klarna/react-native-klarna-network-core';
38
45
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
46
+ export function CheckoutMessaging({ sdk }: { sdk: Klarna }) {
47
+ return (
48
+ <KlarnaMessagingPlacementView
49
+ instance={sdk}
50
+ configuration={{
51
+ type: KlarnaMessagingPlacementType.CreditPromotionAutoSize,
52
+ amount: 19900,
53
+ currency: 'EUR',
54
+ theme: KlarnaTheme.Automatic,
55
+ }}
56
+ onError={(error) => {
57
+ console.warn(error.name, error.message);
58
+ }}
59
+ />
60
+ );
61
+ }
62
+ ```
42
63
 
43
- ---
64
+ **Props (high level)**
44
65
 
45
- **Maintained for OIDC setup purposes only**
66
+ | Prop | Notes |
67
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `instance` | Required. A `Klarna` instance obtained from `@klarna/react-native-klarna-network-core` after initialization. |
69
+ | `configuration` | Required. `{ type, amount, currency, theme? }`. `type` is a `KlarnaMessagingPlacementType`; `theme` is optional — when omitted, the native SDK chooses the default. |
70
+ | `onError` | Optional. Receives `KlarnaMessagingError` (`name`, `message`) when native messaging fails. |
71
+ | `style` | Optional. Passed through; height is driven by native content after load. |
72
+
73
+ ## Support
74
+
75
+ If you are having any issues using the SDK in your project or if you think that something is wrong with the SDK itself, please follow our [support guide](https://github.com/klarna/react-native-klarna-inapp-sdk/blob/master/SUPPORT.md).
76
+
77
+ ## Contribution
78
+
79
+ If you want to contribute to this project please follow our [contribution guide](https://github.com/klarna/react-native-klarna-inapp-sdk/blob/master/CONTRIBUTING.md).
80
+
81
+ ## License
82
+
83
+ This project is licensed under
84
+ [Apache License, Version 2.0](https://github.com/klarna/react-native-klarna-inapp-sdk/blob/master/LICENSE).
85
+
86
+ <!-- Markdown images & links -->
87
+
88
+ [npm-badge]: https://img.shields.io/npm/v/@klarna/react-native-klarna-network-messaging?style=flat-square
89
+ [npm-url]: https://www.npmjs.com/package/@klarna/react-native-klarna-network-messaging
90
+ [dependency-badge]: https://img.shields.io/npm/dependency-version/@klarna/react-native-klarna-network-messaging/peer/react-native?style=flat-square
91
+ [dependency-url]: https://www.npmjs.com/package/@klarna/react-native-klarna-network-messaging?activeTab=dependencies
92
+ [platform-badge]: https://img.shields.io/badge/platform-React%20Native-lightgrey?style=flat-square
93
+ [platform-url]: https://reactnative.dev
94
+ [license-badge]: https://img.shields.io/github/license/klarna/react-native-klarna-inapp-sdk?style=flat-square
95
+ [license-url]: https://github.com/klarna/react-native-klarna-inapp-sdk/blob/master/LICENSE
96
+ [klarna-badge]: https://img.shields.io/badge/%20-Developed%20at%20Klarna-black?labelColor=ffb3c7&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAALQAAAAAQAAAtAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABCgAwAEAAAAAQAAAA4AAAAA0LMKiwAAAAlwSFlzAABuugAAbroB1t6xFwAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAAVBJREFUKBVtkz0vREEUhsdXgo5qJXohkUgQ0fgFNFpR2V5ClP6CQu9PiB6lEL1I7B9A4/treZ47c252s97k2ffMmZkz5869m1JKL/AFbzAHaiRbmsIf4BdaMAZqMFsOXNxXkroKbxCPV5l8yHOJLVipn9/vEreLa7FguSN3S2ynA/ATeQuI8tTY6OOY34DQaQnq9mPCDtxoBwuRxPfAvPMWnARlB12KAi6eLTPruOOP4gcl33O6+Sjgc83DJkRH+h2MgorLzaPy68W48BG2S+xYnmAa1L+nOxEduMH3fgjGFvZeVkANZau68B6CrgJxWosFFpF7iG+h5wKZqwt42qIJtARu/ix+gqsosEq8D35o6R3c7OL4lAnTDljEe9B3Qa2BYzmHemDCt6Diwo6JY7E+A82OnN9HuoBruAQvUQ1nSxP4GVzBDRyBfygf6RW2/gD3NmEv+K/DZgAAAABJRU5ErkJggg==
97
+ [klarna-url]: https://github.com/klarna
@@ -0,0 +1,97 @@
1
+ def klarnaNetworkCorePkgJson = providers.exec {
2
+ commandLine "node", "--print",
3
+ "require.resolve('@klarna/react-native-klarna-network-core/package.json')"
4
+ }.standardOutput.asText.get().trim()
5
+
6
+ apply from: "${file(klarnaNetworkCorePkgJson).parent}/android/klarna-network-versions.gradle"
7
+
8
+ buildscript {
9
+ ext.KlarnaNetworkMessaging = [
10
+ kotlinVersion: "2.0.21",
11
+ minSdkVersion: 21,
12
+ compileSdkVersion: 36,
13
+ targetSdkVersion: 36
14
+ ]
15
+
16
+ ext.getExtOrDefault = { prop ->
17
+ if (rootProject.ext.has(prop)) {
18
+ return rootProject.ext.get(prop)
19
+ }
20
+
21
+ return KlarnaNetworkMessaging[prop]
22
+ }
23
+
24
+ repositories {
25
+ google()
26
+ mavenCentral()
27
+ }
28
+
29
+ dependencies {
30
+ classpath "com.android.tools.build:gradle:8.7.2"
31
+ // noinspection DifferentKotlinGradleVersion
32
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
33
+ }
34
+ }
35
+
36
+ apply plugin: "com.android.library"
37
+ apply plugin: "kotlin-android"
38
+ apply plugin: "com.facebook.react"
39
+
40
+ android {
41
+ namespace "com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging"
42
+
43
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
44
+
45
+ defaultConfig {
46
+ minSdkVersion getExtOrDefault("minSdkVersion")
47
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
48
+ }
49
+
50
+ buildFeatures {
51
+ buildConfig true
52
+ }
53
+
54
+ buildTypes {
55
+ release {
56
+ minifyEnabled false
57
+ }
58
+ }
59
+
60
+ lint {
61
+ disable "GradleCompatible"
62
+ }
63
+
64
+ testOptions {
65
+ unitTests.returnDefaultValues = true
66
+ }
67
+
68
+ compileOptions {
69
+ sourceCompatibility JavaVersion.VERSION_1_8
70
+ targetCompatibility JavaVersion.VERSION_1_8
71
+ }
72
+
73
+ }
74
+
75
+ repositories {
76
+ mavenCentral()
77
+ google()
78
+ maven {
79
+ url 'https://x.klarnacdn.net/mobile-sdk/'
80
+ }
81
+ }
82
+
83
+ dependencies {
84
+ implementation "com.facebook.react:react-android"
85
+
86
+ implementation "com.klarna.mobile.sdk:klarna-network-messaging:${klarnaNetworkVersions.klarnaMobileSdkVersion}"
87
+ implementation "com.klarna.mobile.sdk:klarna-network-core:${klarnaNetworkVersions.klarnaMobileSdkVersion}"
88
+
89
+ // Sibling RN package — needed to call KlarnaNetworkCoreModuleImpl.getInstance()
90
+ // when resolving the active Klarna instance by instanceId. compileOnly is
91
+ // sufficient because the consuming app pulls core in via RN autolinking.
92
+ compileOnly project(":klarna_react-native-klarna-network-core")
93
+
94
+ testImplementation "junit:junit:4.13.2"
95
+ testImplementation "org.mockito:mockito-core:3.12.4"
96
+ testImplementation "org.mockito:mockito-inline:3.12.4"
97
+ }
@@ -0,0 +1 @@
1
+ <manifest />
@@ -0,0 +1,25 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class KlarnaMessagingPlacementErrorEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val errorName: String,
11
+ private val errorMessage: String
12
+ ) : Event<KlarnaMessagingPlacementErrorEvent>(surfaceId, viewTag) {
13
+
14
+ override fun getEventName(): String = "onError"
15
+
16
+ override fun getEventData(): WritableMap {
17
+ val errorMap = Arguments.createMap()
18
+ errorMap.putString("name", errorName)
19
+ errorMap.putString("message", errorMessage)
20
+
21
+ val data = Arguments.createMap()
22
+ data.putMap("error", errorMap)
23
+ return data
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ class KlarnaMessagingPlacementResizedEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val height: String
11
+ ) : Event<KlarnaMessagingPlacementResizedEvent>(surfaceId, viewTag) {
12
+
13
+ override fun getEventName(): String = "onResized"
14
+
15
+ override fun getEventData(): WritableMap {
16
+ val data = Arguments.createMap()
17
+ data.putString("height", height)
18
+ return data
19
+ }
20
+ }
@@ -0,0 +1,251 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.view.Choreographer
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import android.view.ViewTreeObserver
9
+ import android.widget.FrameLayout
10
+ import com.facebook.react.bridge.ReactContext
11
+ import com.facebook.react.uimanager.PixelUtil
12
+ import com.facebook.react.uimanager.ThemedReactContext
13
+ import com.facebook.react.uimanager.UIManagerHelper
14
+ import com.facebook.react.uimanager.annotations.ReactProp
15
+ import com.klarna.mobile.sdk.klarna.network.messaging.api.KlarnaMessagingPlacementView
16
+ import com.klarna.mobile.sdk.reactnative.klarnanetwork.core.KlarnaNetworkCoreModuleImpl
17
+
18
+ class KlarnaMessagingPlacementViewManager :
19
+ RNKlarnaMessagingPlacementViewSpec<FrameLayout>() {
20
+
21
+ companion object {
22
+ const val REACT_CLASS = "RNKlarnaMessagingPlacementView"
23
+
24
+ /**
25
+ * Staggered post times (ms) for [reportFromActualLayout] retries. Messaging content
26
+ * can load after the first layout pass, so we schedule extra measurements alongside
27
+ * the pre-draw listener.
28
+ */
29
+ private val HEIGHT_CHECK_DELAYS_MS = longArrayOf(100, 500, 1500, 3000, 6000)
30
+ }
31
+
32
+ private val handler = Handler(Looper.getMainLooper())
33
+
34
+ private val viewToInstanceId = mutableMapOf<FrameLayout, String>()
35
+ private val viewToPlacementType = mutableMapOf<FrameLayout, String>()
36
+ private val viewToTheme = mutableMapOf<FrameLayout, String>()
37
+ private val viewToAmount = mutableMapOf<FrameLayout, String>()
38
+ private val viewToCurrency = mutableMapOf<FrameLayout, String>()
39
+ private val pendingRecreate = mutableMapOf<FrameLayout, Boolean>()
40
+ // Pair the listener with the View it was registered on. removeLayoutListener
41
+ // must remove from the same ViewTreeObserver — looking up wrapper.getChildAt(0)
42
+ // is unreliable: recreatePlacementView calls removeAllViews() before
43
+ // removeLayoutListener, leaving childCount == 0 and orphaning the listener.
44
+ private val preDrawListeners = mutableMapOf<FrameLayout, Pair<View, ViewTreeObserver.OnPreDrawListener>>()
45
+ private val lastReportedHeight = mutableMapOf<FrameLayout, Int>()
46
+
47
+ override fun getName(): String = REACT_CLASS
48
+
49
+ override fun createViewInstance(themedReactContext: ThemedReactContext): FrameLayout {
50
+ return FrameLayout(themedReactContext)
51
+ }
52
+
53
+ /** Cleans up listeners, pending handler callbacks, and prop maps when React drops the view. */
54
+ override fun onDropViewInstance(view: FrameLayout) {
55
+ removeLayoutListener(view)
56
+ handler.removeCallbacksAndMessages(view)
57
+ pendingRecreate.remove(view)
58
+ viewToInstanceId.remove(view)
59
+ viewToPlacementType.remove(view)
60
+ viewToTheme.remove(view)
61
+ viewToAmount.remove(view)
62
+ viewToCurrency.remove(view)
63
+ lastReportedHeight.remove(view)
64
+ super.onDropViewInstance(view)
65
+ }
66
+
67
+ @ReactProp(name = "instanceId")
68
+ override fun setInstanceId(view: FrameLayout, value: String?) {
69
+ if (value != null) viewToInstanceId[view] = value else viewToInstanceId.remove(view)
70
+ scheduleRecreate(view)
71
+ }
72
+
73
+ @ReactProp(name = "placementType")
74
+ override fun setPlacementType(view: FrameLayout, value: String?) {
75
+ if (value != null) viewToPlacementType[view] = value else viewToPlacementType.remove(view)
76
+ scheduleRecreate(view)
77
+ }
78
+
79
+ @ReactProp(name = "theme")
80
+ override fun setTheme(view: FrameLayout, value: String?) {
81
+ if (value != null) viewToTheme[view] = value else viewToTheme.remove(view)
82
+ scheduleRecreate(view)
83
+ }
84
+
85
+ @ReactProp(name = "amount")
86
+ override fun setAmount(view: FrameLayout, value: String?) {
87
+ if (value != null) viewToAmount[view] = value else viewToAmount.remove(view)
88
+ scheduleRecreate(view)
89
+ }
90
+
91
+ @ReactProp(name = "currency")
92
+ override fun setCurrency(view: FrameLayout, value: String?) {
93
+ if (value != null) viewToCurrency[view] = value else viewToCurrency.remove(view)
94
+ scheduleRecreate(view)
95
+ }
96
+
97
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> =
98
+ mapOf(
99
+ "onError" to mapOf("registrationName" to "onError"),
100
+ "onResized" to mapOf("registrationName" to "onResized")
101
+ )
102
+
103
+ /**
104
+ * Schedules placement view recreation for the next frame so React prop batches
105
+ * can settle first. Subsequent prop changes within the same frame coalesce into
106
+ * a single recreate pass.
107
+ */
108
+ private fun scheduleRecreate(wrapper: FrameLayout) {
109
+ if (pendingRecreate[wrapper] == true) return
110
+ pendingRecreate[wrapper] = true
111
+ Choreographer.getInstance().postFrameCallback {
112
+ pendingRecreate.remove(wrapper)
113
+ recreatePlacementView(wrapper)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Tears down any existing placement view inside [wrapper] and builds a fresh one
119
+ * from the latest React props. Returns silently when amount, currency, or
120
+ * instanceId are missing; dispatches a `NotInitialized` error event when the
121
+ * Klarna instance can't be resolved. After adding the new SDK view it attaches
122
+ * a measurement observer so height changes are reported back to React.
123
+ */
124
+ private fun recreatePlacementView(wrapper: FrameLayout) {
125
+ val amount = KlarnaMessagingViewManagerHelpers.parseAmount(viewToAmount[wrapper]) ?: return
126
+ val currency = viewToCurrency[wrapper] ?: return
127
+ if (currency.isEmpty()) return
128
+
129
+ removeLayoutListener(wrapper)
130
+ wrapper.removeAllViews()
131
+ handler.removeCallbacksAndMessages(wrapper)
132
+ lastReportedHeight.remove(wrapper)
133
+
134
+ val instanceId = viewToInstanceId[wrapper]
135
+ if (instanceId.isNullOrEmpty()) return
136
+ val klarnaInstance = KlarnaNetworkCoreModuleImpl.getInstance(instanceId)
137
+ if (klarnaInstance == null) {
138
+ dispatchError(
139
+ wrapper,
140
+ "NotInitialized",
141
+ "No Klarna instance found for instanceId. Call Klarna.initialize() first."
142
+ )
143
+ return
144
+ }
145
+
146
+ val theme = KlarnaMessagingViewManagerHelpers.parseTheme(viewToTheme[wrapper])
147
+ val configuration = KlarnaMessagingViewManagerHelpers.buildConfiguration(
148
+ viewToPlacementType[wrapper],
149
+ theme,
150
+ amount,
151
+ currency
152
+ )
153
+
154
+ val placementView = KlarnaMessagingPlacementView(wrapper.context, klarnaInstance, configuration)
155
+ wrapper.addView(placementView, FrameLayout.LayoutParams(
156
+ FrameLayout.LayoutParams.MATCH_PARENT,
157
+ FrameLayout.LayoutParams.WRAP_CONTENT
158
+ ))
159
+ attachMeasurementObserver(wrapper, placementView)
160
+ }
161
+
162
+ /**
163
+ * Registers two complementary height-tracking strategies for [placementView]:
164
+ * 1. An [ViewTreeObserver.OnPreDrawListener] that fires on every layout pass.
165
+ * 2. A series of staggered [Handler] callbacks ([HEIGHT_CHECK_DELAYS_MS]) to catch
166
+ * asynchronous content loads that may not trigger a layout pass immediately.
167
+ */
168
+ private fun attachMeasurementObserver(wrapper: FrameLayout, placementView: View) {
169
+ val listener = ViewTreeObserver.OnPreDrawListener {
170
+ reportFromActualLayout(wrapper, placementView)
171
+ true
172
+ }
173
+ placementView.viewTreeObserver.addOnPreDrawListener(listener)
174
+ preDrawListeners[wrapper] = placementView to listener
175
+
176
+ for ((index, delay) in HEIGHT_CHECK_DELAYS_MS.withIndex()) {
177
+ val isLastCheck = index == HEIGHT_CHECK_DELAYS_MS.size - 1
178
+ val runnable = Runnable {
179
+ reportFromActualLayout(wrapper, placementView)
180
+ if (isLastCheck && lastReportedHeight[wrapper] == null) {
181
+ dispatchResized(wrapper, 0)
182
+ }
183
+ }
184
+ handler.postAtTime(runnable, wrapper, android.os.SystemClock.uptimeMillis() + delay)
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Manually measures [placementView] against [wrapper]'s width with unconstrained height,
190
+ * then dispatches an `onResized` event if the content height (in dp) changed since the
191
+ * last report. A manual measure is needed because React Native sizes the wrapper via
192
+ * `setFrame()`, so the SDK view never gets a standard measure pass with the real width.
193
+ */
194
+ private fun reportFromActualLayout(wrapper: FrameLayout, placementView: View) {
195
+ if (!placementView.isAttachedToWindow) return
196
+
197
+ val width = wrapper.width
198
+ if (width <= 0) return
199
+
200
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
201
+ val heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
202
+ placementView.measure(widthSpec, heightSpec)
203
+ placementView.layout(0, 0, placementView.measuredWidth, placementView.measuredHeight)
204
+
205
+ val contentHeight = placementView.measuredHeight
206
+ if (contentHeight <= 0) return
207
+
208
+ val heightDp = Math.round(PixelUtil.toDIPFromPixel(contentHeight.toFloat()))
209
+ if (!KlarnaMessagingViewManagerHelpers.shouldReportHeight(lastReportedHeight[wrapper], heightDp)) {
210
+ return
211
+ }
212
+ lastReportedHeight[wrapper] = heightDp
213
+ dispatchResized(wrapper, heightDp)
214
+ }
215
+
216
+ private fun dispatchResized(wrapper: FrameLayout, heightDp: Int) {
217
+ val dispatcher = resolveDispatcher(wrapper) ?: return
218
+ val surfaceId = UIManagerHelper.getSurfaceId(wrapper)
219
+ dispatcher.dispatchEvent(
220
+ KlarnaMessagingPlacementResizedEvent(surfaceId, wrapper.id, heightDp.toString())
221
+ )
222
+ }
223
+
224
+ private fun dispatchError(wrapper: FrameLayout, name: String, message: String) {
225
+ val dispatcher = resolveDispatcher(wrapper) ?: return
226
+ val surfaceId = UIManagerHelper.getSurfaceId(wrapper)
227
+ dispatcher.dispatchEvent(
228
+ KlarnaMessagingPlacementErrorEvent(surfaceId, wrapper.id, name, message)
229
+ )
230
+ }
231
+
232
+ private fun resolveDispatcher(wrapper: FrameLayout) =
233
+ UIManagerHelper.getEventDispatcherForReactTag(
234
+ wrapper.context as ReactContext,
235
+ wrapper.id
236
+ )
237
+
238
+ /**
239
+ * Removes the pre-draw listener previously registered for [wrapper]. The listener is
240
+ * removed from the [ViewTreeObserver] of the original view it was attached to (stored
241
+ * in [preDrawListeners]), not the current child, because [recreatePlacementView] may
242
+ * have already called `removeAllViews()`.
243
+ */
244
+ private fun removeLayoutListener(wrapper: FrameLayout) {
245
+ val (target, listener) = preDrawListeners.remove(wrapper) ?: return
246
+ val observer = target.viewTreeObserver
247
+ if (observer.isAlive) {
248
+ observer.removeOnPreDrawListener(listener)
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,52 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import com.klarna.mobile.sdk.api.KlarnaTheme
4
+ import com.klarna.mobile.sdk.klarna.network.messaging.api.KlarnaMessagingPlacementConfiguration
5
+
6
+ internal enum class PlacementType(val value: String) {
7
+ AUTO_SIZE("CreditPromotionAutoSize"),
8
+ BADGE("CreditPromotionBadge");
9
+
10
+ companion object {
11
+ fun fromString(value: String?): PlacementType =
12
+ entries.find { it.value == value } ?: AUTO_SIZE
13
+ }
14
+ }
15
+
16
+ internal object KlarnaMessagingViewManagerHelpers {
17
+
18
+ fun parseTheme(value: String?): KlarnaTheme? = when (value) {
19
+ "light" -> KlarnaTheme.LIGHT
20
+ "dark" -> KlarnaTheme.DARK
21
+ "automatic" -> KlarnaTheme.AUTOMATIC
22
+ else -> null
23
+ }
24
+
25
+ fun parseAmount(value: String?): Long? {
26
+ if (value.isNullOrEmpty()) return null
27
+ return value.toLongOrNull()
28
+ }
29
+
30
+ fun resolvePlacementType(value: String?): PlacementType =
31
+ PlacementType.fromString(value)
32
+
33
+ fun buildConfiguration(
34
+ placementType: String?,
35
+ theme: KlarnaTheme?,
36
+ amount: Long,
37
+ currency: String
38
+ ): KlarnaMessagingPlacementConfiguration =
39
+ when (resolvePlacementType(placementType)) {
40
+ PlacementType.BADGE ->
41
+ KlarnaMessagingPlacementConfiguration.CreditPromotionBadge(theme, amount, currency)
42
+ PlacementType.AUTO_SIZE ->
43
+ KlarnaMessagingPlacementConfiguration.CreditPromotionAutoSize(theme, amount, currency)
44
+ }
45
+
46
+ /**
47
+ * Returns true when an incoming height report should be dispatched
48
+ * (i.e. it differs from the last value already reported).
49
+ */
50
+ fun shouldReportHeight(lastReported: Int?, newHeightDp: Int): Boolean =
51
+ newHeightDp > 0 && lastReported != newHeightDp
52
+ }
@@ -0,0 +1,19 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class KlarnaNetworkMessagingPackage : BaseReactPackage() {
10
+
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
12
+ null
13
+
14
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
15
+ ReactModuleInfoProvider { emptyMap() }
16
+
17
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
18
+ listOf<ViewManager<*, *>>(KlarnaMessagingPlacementViewManager())
19
+ }
@@ -0,0 +1,22 @@
1
+ package com.klarna.mobile.sdk.reactnative.klarnanetwork.messaging
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.ReadableArray
5
+ import com.facebook.react.uimanager.SimpleViewManager
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.viewmanagers.RNKlarnaMessagingPlacementViewManagerDelegate
8
+ import com.facebook.react.viewmanagers.RNKlarnaMessagingPlacementViewManagerInterface
9
+
10
+ abstract class RNKlarnaMessagingPlacementViewSpec<T : View> :
11
+ SimpleViewManager<T>(),
12
+ RNKlarnaMessagingPlacementViewManagerInterface<T> {
13
+
14
+ private val delegate: ViewManagerDelegate<T> =
15
+ RNKlarnaMessagingPlacementViewManagerDelegate(this)
16
+
17
+ override fun getDelegate(): ViewManagerDelegate<T> = delegate
18
+
19
+ override fun receiveCommand(root: T, commandId: String, args: ReadableArray) {
20
+ delegate.receiveCommand(root, commandId, args)
21
+ }
22
+ }
@@ -0,0 +1,39 @@
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.Before
8
+ import org.junit.Test
9
+ import org.mockito.MockedStatic
10
+ import org.mockito.Mockito
11
+
12
+ class KlarnaMessagingPlacementEventsTest {
13
+
14
+ private lateinit var mockedArguments: MockedStatic<Arguments>
15
+
16
+ @Before
17
+ fun setUp() {
18
+ mockedArguments = Mockito.mockStatic(Arguments::class.java)
19
+ mockedArguments.`when`<JavaOnlyMap> { Arguments.createMap() }
20
+ .thenAnswer { JavaOnlyMap() }
21
+ }
22
+
23
+ @After
24
+ fun tearDown() {
25
+ mockedArguments.close()
26
+ }
27
+
28
+ @Test
29
+ fun resizedEvent_eventName() {
30
+ val event = KlarnaMessagingPlacementResizedEvent(0, 1, "120")
31
+ assertEquals("onResized", event.eventName)
32
+ }
33
+
34
+ @Test
35
+ fun errorEvent_eventName() {
36
+ val event = KlarnaMessagingPlacementErrorEvent(0, 1, "TestError", "Something went wrong")
37
+ assertEquals("onError", event.eventName)
38
+ }
39
+ }