@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.
@@ -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
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ module.exports = require("./plugin");
@@ -26,12 +26,12 @@ class PNLightRNModule: NSObject {
26
26
  }
27
27
  }
28
28
 
29
- @objc func getUserId(_ resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
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(_ resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
34
+ @objc func resetUserId(resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
35
35
  PNLightSDK.shared.resetUserId()
36
36
  resolve(nil)
37
37
  }
@@ -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 nothing until config is set; then renders via DivKit per https://divkit.tech/docs/en/quickstart/ios
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
- addSubview(divView)
18
- NSLayoutConstraint.activate([
19
- divView.topAnchor.constraint(equalTo: topAnchor),
20
- divView.leadingAnchor.constraint(equalTo: leadingAnchor),
21
- divView.trailingAnchor.constraint(equalTo: trailingAnchor),
22
- divView.bottomAnchor.constraint(equalTo: bottomAnchor),
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
- divView.setSource(DivViewSource(kind: .data(jsonData), cardId: cardId))
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.0",
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",
@@ -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;