@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
package/README.md
CHANGED
|
@@ -1,45 +1,97 @@
|
|
|
1
1
|
# @klarna/react-native-klarna-network-messaging
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
13
|
+
## Requirements
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
20
|
+
### NPM
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
```shell
|
|
23
|
+
npm install @klarna/react-native-klarna-network-messaging --save
|
|
24
|
+
```
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
### Yarn
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
```shell
|
|
29
|
+
yarn add @klarna/react-native-klarna-network-messaging
|
|
30
|
+
```
|
|
23
31
|
|
|
24
|
-
|
|
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
|
-
##
|
|
34
|
+
## Usage
|
|
30
35
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
+
}
|