@pnlight/sdk-react-native 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RemoteUiView.js +43 -0
- package/app.plugin.js +3 -0
- package/ios/PNLightSDK.swift +2 -2
- package/ios/RemoteUiView.swift +122 -9
- package/package.json +16 -3
- package/plugin/index.js +17 -0
- package/plugin/withIos.js +39 -0
package/RemoteUiView.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const React = require("react");
|
|
4
|
+
const { requireNativeComponent } = require("react-native");
|
|
5
|
+
const { getUIConfig } = require(".");
|
|
6
|
+
|
|
7
|
+
const NativeRemoteUiView = requireNativeComponent("RemoteUiView");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Remote UI view: fetches UI config for the given placement via getUIConfig,
|
|
11
|
+
* shows nothing while loading, then renders the config in a native DivKit view.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} props
|
|
14
|
+
* @param {string} props.placement - Placement id passed to getUIConfig(placement)
|
|
15
|
+
* @param {object} props.style - Optional style for the container
|
|
16
|
+
* @param {string} [props.cardId] - Optional DivKit card id (defaults to placement-based)
|
|
17
|
+
*/
|
|
18
|
+
function RemoteUiView({ placement, style, cardId }) {
|
|
19
|
+
const [config, setConfig] = React.useState(null);
|
|
20
|
+
const cardIdToUse = cardId ?? (placement ? `pnlight_${placement}` : "pnlight_card");
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!placement) return;
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
getUIConfig(placement).then((uiConfig) => {
|
|
26
|
+
if (cancelled) return;
|
|
27
|
+
setConfig(uiConfig?.config ?? null);
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
cancelled = true;
|
|
31
|
+
};
|
|
32
|
+
}, [placement]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<NativeRemoteUiView
|
|
36
|
+
style={[{ minHeight: 1 }, style]}
|
|
37
|
+
config={config}
|
|
38
|
+
cardId={cardIdToUse}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { RemoteUiView };
|
package/app.plugin.js
ADDED
package/ios/PNLightSDK.swift
CHANGED
|
@@ -26,12 +26,12 @@ class PNLightRNModule: NSObject {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
@objc func getUserId(
|
|
29
|
+
@objc func getUserId(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
30
30
|
let userId = PNLightSDK.shared.getOrCreateUserId().id
|
|
31
31
|
resolve(userId)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
@objc func resetUserId(
|
|
34
|
+
@objc func resetUserId(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
35
35
|
PNLightSDK.shared.resetUserId()
|
|
36
36
|
resolve(nil)
|
|
37
37
|
}
|
package/ios/RemoteUiView.swift
CHANGED
|
@@ -2,7 +2,7 @@ import UIKit
|
|
|
2
2
|
import DivKit
|
|
3
3
|
|
|
4
4
|
/// Native UIView that renders PNLight UI config (DivKit JSON) inside a DivView.
|
|
5
|
-
/// Shows
|
|
5
|
+
/// Shows loading until config is set and loaded; supports error state and optional secure container.
|
|
6
6
|
@objc(RemoteUiView)
|
|
7
7
|
final class RemoteUiView: UIView {
|
|
8
8
|
|
|
@@ -10,30 +10,143 @@ final class RemoteUiView: UIView {
|
|
|
10
10
|
private static let divKitComponents = DivKitComponents()
|
|
11
11
|
private var currentCardId: String = "pnlight_card"
|
|
12
12
|
|
|
13
|
+
private let loadingIndicator: UIActivityIndicatorView
|
|
14
|
+
private let errorLabel: UILabel
|
|
15
|
+
private var secureContainer: UIView?
|
|
16
|
+
|
|
13
17
|
override init(frame: CGRect) {
|
|
14
18
|
self.divView = DivView(divKitComponents: Self.divKitComponents)
|
|
19
|
+
self.loadingIndicator = UIActivityIndicatorView(style: .large)
|
|
20
|
+
self.errorLabel = UILabel()
|
|
21
|
+
|
|
15
22
|
super.init(frame: frame)
|
|
23
|
+
|
|
24
|
+
backgroundColor = .clear
|
|
25
|
+
|
|
16
26
|
divView.translatesAutoresizingMaskIntoConstraints = false
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
divView.alpha = 0
|
|
28
|
+
divView.backgroundColor = .clear
|
|
29
|
+
|
|
30
|
+
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
31
|
+
loadingIndicator.hidesWhenStopped = false
|
|
32
|
+
|
|
33
|
+
errorLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
34
|
+
errorLabel.text = "Failed to load DivKit content"
|
|
35
|
+
errorLabel.textColor = .red
|
|
36
|
+
errorLabel.textAlignment = .center
|
|
37
|
+
errorLabel.numberOfLines = 0
|
|
38
|
+
errorLabel.alpha = 0
|
|
39
|
+
|
|
40
|
+
let isSecure = true
|
|
41
|
+
if isSecure, let secure = Self.makeSecureContainer() {
|
|
42
|
+
secure.translatesAutoresizingMaskIntoConstraints = false
|
|
43
|
+
self.secureContainer = secure
|
|
44
|
+
|
|
45
|
+
secure.addSubview(divView)
|
|
46
|
+
secure.addSubview(loadingIndicator)
|
|
47
|
+
secure.addSubview(errorLabel)
|
|
48
|
+
addSubview(secure)
|
|
49
|
+
|
|
50
|
+
NSLayoutConstraint.activate([
|
|
51
|
+
secure.topAnchor.constraint(equalTo: topAnchor),
|
|
52
|
+
secure.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
53
|
+
secure.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
54
|
+
secure.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
55
|
+
divView.topAnchor.constraint(equalTo: secure.topAnchor),
|
|
56
|
+
divView.leadingAnchor.constraint(equalTo: secure.leadingAnchor),
|
|
57
|
+
divView.trailingAnchor.constraint(equalTo: secure.trailingAnchor),
|
|
58
|
+
divView.bottomAnchor.constraint(equalTo: secure.bottomAnchor),
|
|
59
|
+
loadingIndicator.centerXAnchor.constraint(equalTo: secure.centerXAnchor),
|
|
60
|
+
loadingIndicator.centerYAnchor.constraint(equalTo: secure.centerYAnchor),
|
|
61
|
+
errorLabel.topAnchor.constraint(equalTo: secure.topAnchor),
|
|
62
|
+
errorLabel.leadingAnchor.constraint(equalTo: secure.leadingAnchor),
|
|
63
|
+
errorLabel.trailingAnchor.constraint(equalTo: secure.trailingAnchor),
|
|
64
|
+
errorLabel.bottomAnchor.constraint(equalTo: secure.bottomAnchor),
|
|
65
|
+
])
|
|
66
|
+
} else {
|
|
67
|
+
addSubview(divView)
|
|
68
|
+
addSubview(loadingIndicator)
|
|
69
|
+
addSubview(errorLabel)
|
|
70
|
+
|
|
71
|
+
NSLayoutConstraint.activate([
|
|
72
|
+
divView.topAnchor.constraint(equalTo: topAnchor),
|
|
73
|
+
divView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
74
|
+
divView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
75
|
+
divView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
76
|
+
loadingIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
77
|
+
loadingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
78
|
+
errorLabel.topAnchor.constraint(equalTo: topAnchor),
|
|
79
|
+
errorLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
80
|
+
errorLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
81
|
+
errorLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
82
|
+
])
|
|
83
|
+
}
|
|
24
84
|
}
|
|
25
85
|
|
|
26
86
|
required init?(coder: NSCoder) {
|
|
27
87
|
fatalError("init(coder:) has not been implemented")
|
|
28
88
|
}
|
|
29
89
|
|
|
90
|
+
/// Creates a secure container that hides content during screenshots and screen recording.
|
|
91
|
+
private static func makeSecureContainer() -> UIView? {
|
|
92
|
+
let textField = UITextField()
|
|
93
|
+
textField.isSecureTextEntry = true
|
|
94
|
+
textField.isUserInteractionEnabled = false
|
|
95
|
+
|
|
96
|
+
guard let secureLayer = textField.layer.sublayers?.first,
|
|
97
|
+
let secureView = secureLayer.delegate as? UIView else {
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
secureView.subviews.forEach { $0.removeFromSuperview() }
|
|
102
|
+
secureView.isUserInteractionEnabled = true
|
|
103
|
+
secureView.backgroundColor = .clear
|
|
104
|
+
return secureView
|
|
105
|
+
}
|
|
106
|
+
|
|
30
107
|
/// Renders the given DivKit JSON string. Pass nil to show blank (e.g. while loading).
|
|
31
108
|
private func applyConfig(configJson: String?, cardId: String) {
|
|
32
109
|
guard let configJson = configJson, !configJson.isEmpty,
|
|
33
110
|
let jsonData = configJson.data(using: .utf8) else {
|
|
34
111
|
return
|
|
35
112
|
}
|
|
36
|
-
|
|
113
|
+
|
|
114
|
+
errorLabel.alpha = 0
|
|
115
|
+
errorLabel.text = "Failed to load DivKit content"
|
|
116
|
+
loadingIndicator.startAnimating()
|
|
117
|
+
loadingIndicator.alpha = 1
|
|
118
|
+
divView.alpha = 0
|
|
119
|
+
|
|
120
|
+
let source = DivViewSource(
|
|
121
|
+
kind: .data(jsonData),
|
|
122
|
+
cardId: DivCardID(rawValue: cardId) ?? "divkit"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
Task { @MainActor in
|
|
126
|
+
do {
|
|
127
|
+
try await self.divView.setSource(source)
|
|
128
|
+
self.showContent()
|
|
129
|
+
} catch {
|
|
130
|
+
self.showError(error: error)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func showContent() {
|
|
136
|
+
loadingIndicator.stopAnimating()
|
|
137
|
+
UIView.animate(withDuration: 0.3) {
|
|
138
|
+
self.loadingIndicator.alpha = 0
|
|
139
|
+
self.divView.alpha = 1
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private func showError(error: Error) {
|
|
144
|
+
errorLabel.text = "Failed to load DivKit content:\n\(error.localizedDescription)"
|
|
145
|
+
loadingIndicator.stopAnimating()
|
|
146
|
+
UIView.animate(withDuration: 0.3) {
|
|
147
|
+
self.loadingIndicator.alpha = 0
|
|
148
|
+
self.errorLabel.alpha = 1
|
|
149
|
+
}
|
|
37
150
|
}
|
|
38
151
|
|
|
39
152
|
// MARK: - React Native view props
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnlight/sdk-react-native",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "React Native wrapper for PNLight iOS binary SDK",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,16 +10,29 @@
|
|
|
10
10
|
"index.d.ts",
|
|
11
11
|
"ios",
|
|
12
12
|
"PNLight.xcframework",
|
|
13
|
-
"PNLightSDK-ReactNative.podspec"
|
|
13
|
+
"PNLightSDK-ReactNative.podspec",
|
|
14
|
+
"RemoteUiView.js",
|
|
15
|
+
"app.plugin.js",
|
|
16
|
+
"plugin"
|
|
14
17
|
],
|
|
15
18
|
"keywords": [
|
|
16
19
|
"pnlight",
|
|
17
20
|
"analytics",
|
|
18
21
|
"in-app-purchase",
|
|
19
|
-
"ios"
|
|
22
|
+
"ios",
|
|
23
|
+
"expo"
|
|
20
24
|
],
|
|
21
25
|
"author": "PNLight",
|
|
22
26
|
"license": "Commercial",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react-native": "*",
|
|
29
|
+
"expo": "*"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"expo": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
23
36
|
"scripts": {
|
|
24
37
|
"prepare": "node ./scripts/copy-xcframework.js",
|
|
25
38
|
"publish:dry": "npm pack",
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { withIosPodfileSource } = require("./withIos");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expo config plugin for @pnlight/sdk-react-native.
|
|
7
|
+
* Configures the iOS Podfile with the DivKit source required by RemoteUiView.
|
|
8
|
+
*
|
|
9
|
+
* Usage in app.json / app.config.js:
|
|
10
|
+
* "plugins": ["@pnlight/sdk-react-native"]
|
|
11
|
+
*/
|
|
12
|
+
function withPNLightSDK(config) {
|
|
13
|
+
config = withIosPodfileSource(config);
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = withPNLightSDK;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { withDangerousMod } = require("expo/config-plugins");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const DIVKIT_SOURCE = "source 'https://github.com/divkit/divkit-ios.git'";
|
|
8
|
+
const DIVKIT_MARKER = "divkit-ios";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Adds DivKit CocoaPods source to the Podfile (required by PNLightSDK-ReactNative for RemoteUiView).
|
|
12
|
+
* Idempotent: safe to run prebuild multiple times.
|
|
13
|
+
*/
|
|
14
|
+
function withIosPodfileSource(config) {
|
|
15
|
+
return withDangerousMod(config, [
|
|
16
|
+
"ios",
|
|
17
|
+
async (config) => {
|
|
18
|
+
const podfilePath = path.join(
|
|
19
|
+
config.modRequest.platformProjectRoot,
|
|
20
|
+
"Podfile"
|
|
21
|
+
);
|
|
22
|
+
let contents = await fs.promises.readFile(podfilePath, "utf8");
|
|
23
|
+
|
|
24
|
+
if (contents.includes(DIVKIT_MARKER)) {
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const lines = contents.split("\n");
|
|
29
|
+
const insertIndex =
|
|
30
|
+
lines.findIndex((line) => /^\s*source\s+['\"]/.test(line)) + 1;
|
|
31
|
+
const idx = insertIndex > 0 ? insertIndex : 1;
|
|
32
|
+
lines.splice(idx, 0, DIVKIT_SOURCE);
|
|
33
|
+
await fs.promises.writeFile(podfilePath, lines.join("\n"));
|
|
34
|
+
return config;
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = withIosPodfileSource;
|