@pnlight/sdk-react-native 0.4.1 → 0.4.2
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.d.ts +62 -0
- package/RemoteUiView.js +19 -4
- package/ios/PNLightSDK.m +4 -1
- package/ios/RemoteUiView.swift +89 -10
- package/ios/RemoteUiViewManager.m +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { StyleProp, ViewStyle } from "react-native";
|
|
2
|
+
|
|
3
|
+
export interface ActionEvent {
|
|
4
|
+
/**
|
|
5
|
+
* Full URL from the action (e.g., "myapp://button-clicked?id=primary")
|
|
6
|
+
* Note: Only custom URL schemes are passed to this callback.
|
|
7
|
+
* Standard http/https URLs are handled automatically by DivKit.
|
|
8
|
+
*/
|
|
9
|
+
url: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* URL scheme (e.g., "myapp", "app")
|
|
13
|
+
* Note: Will never be "http" or "https" as those are handled automatically.
|
|
14
|
+
*/
|
|
15
|
+
scheme: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* URL path (e.g., "/button-clicked")
|
|
19
|
+
*/
|
|
20
|
+
path: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parsed query parameters as key-value pairs
|
|
24
|
+
*/
|
|
25
|
+
params: { [key: string]: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* DivKit log ID (if available)
|
|
29
|
+
*/
|
|
30
|
+
logId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RemoteUiViewProps {
|
|
34
|
+
/**
|
|
35
|
+
* Placement ID passed to getUIConfig(placement)
|
|
36
|
+
*/
|
|
37
|
+
placement: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Optional style for the container
|
|
41
|
+
*/
|
|
42
|
+
style?: StyleProp<ViewStyle>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Optional DivKit card ID (defaults to placement-based ID)
|
|
46
|
+
*/
|
|
47
|
+
cardId?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Callback for handling button clicks with custom URL schemes.
|
|
51
|
+
* Only called for custom schemes (myapp://, app://, etc.).
|
|
52
|
+
* Standard http/https URLs are handled automatically by DivKit.
|
|
53
|
+
* @param event - Action event containing URL, scheme, path, params, and logId
|
|
54
|
+
*/
|
|
55
|
+
onAction?: (event: ActionEvent) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Remote UI view: fetches UI config for the given placement via getUIConfig,
|
|
60
|
+
* shows nothing while loading, then renders the config in a native DivKit view.
|
|
61
|
+
*/
|
|
62
|
+
export function RemoteUiView(props: RemoteUiViewProps): JSX.Element;
|
package/RemoteUiView.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const React = require("react");
|
|
4
4
|
const { requireNativeComponent } = require("react-native");
|
|
5
|
-
const { getUIConfig } = require(".");
|
|
6
5
|
|
|
7
6
|
const NativeRemoteUiView = requireNativeComponent("RemoteUiView");
|
|
8
7
|
|
|
8
|
+
async function getUIConfig(placement) {
|
|
9
|
+
return await NativeRemoteUiView.getUIConfig(placement);
|
|
10
|
+
}
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* Remote UI view: fetches UI config for the given placement via getUIConfig,
|
|
11
14
|
* shows nothing while loading, then renders the config in a native DivKit view.
|
|
@@ -14,10 +17,12 @@ const NativeRemoteUiView = requireNativeComponent("RemoteUiView");
|
|
|
14
17
|
* @param {string} props.placement - Placement id passed to getUIConfig(placement)
|
|
15
18
|
* @param {object} props.style - Optional style for the container
|
|
16
19
|
* @param {string} [props.cardId] - Optional DivKit card id (defaults to placement-based)
|
|
20
|
+
* @param {function} [props.onAction] - Callback for handling button clicks and custom actions
|
|
17
21
|
*/
|
|
18
|
-
function RemoteUiView({ placement, style, cardId }) {
|
|
22
|
+
function RemoteUiView({ placement, style, cardId, onAction }) {
|
|
19
23
|
const [config, setConfig] = React.useState(null);
|
|
20
|
-
const cardIdToUse =
|
|
24
|
+
const cardIdToUse =
|
|
25
|
+
cardId ?? (placement ? `pnlight_${placement}` : "pnlight_card");
|
|
21
26
|
|
|
22
27
|
React.useEffect(() => {
|
|
23
28
|
if (!placement) return;
|
|
@@ -31,11 +36,21 @@ function RemoteUiView({ placement, style, cardId }) {
|
|
|
31
36
|
};
|
|
32
37
|
}, [placement]);
|
|
33
38
|
|
|
39
|
+
const handleAction = React.useCallback(
|
|
40
|
+
(event) => {
|
|
41
|
+
if (onAction) {
|
|
42
|
+
onAction(event.nativeEvent);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
[onAction],
|
|
46
|
+
);
|
|
47
|
+
|
|
34
48
|
return (
|
|
35
49
|
<NativeRemoteUiView
|
|
36
|
-
style={[{ minHeight: 1 }, style]}
|
|
50
|
+
style={[{ minHeight: 1, flex: 1 }, style]}
|
|
37
51
|
config={config}
|
|
38
52
|
cardId={cardIdToUse}
|
|
53
|
+
onAction={handleAction}
|
|
39
54
|
/>
|
|
40
55
|
);
|
|
41
56
|
}
|
package/ios/PNLightSDK.m
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
@import PNLight;
|
|
3
3
|
|
|
4
4
|
@interface RCT_EXTERN_MODULE(PNLightRN, NSObject)
|
|
5
|
-
RCT_EXTERN_METHOD(initialize:(NSString *)apiKey resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
5
|
+
RCT_EXTERN_METHOD(initialize:(NSString *)apiKey baseDomain:(NSString *)baseDomain resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
6
6
|
RCT_EXTERN_METHOD(validatePurchase:(BOOL)captcha resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
7
7
|
RCT_EXTERN_METHOD(logEvent:(NSString *)eventName eventArgs:(NSDictionary *)eventArgs resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
8
|
+
RCT_EXTERN_METHOD(getUserId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
RCT_EXTERN_METHOD(resetUserId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
RCT_EXTERN_METHOD(addAttribution:(NSString *)providerString data:(NSDictionary *)data identifier:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
8
11
|
RCT_EXTERN_METHOD(prefetchUIConfig:(NSString *)placement resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
12
|
RCT_EXTERN_METHOD(getUIConfig:(NSString *)placement resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
13
|
@end
|
package/ios/RemoteUiView.swift
CHANGED
|
@@ -1,26 +1,71 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
import DivKit
|
|
3
3
|
|
|
4
|
-
/// Native UIView that renders PNLight UI config (DivKit JSON) inside a DivView.
|
|
5
|
-
/// Shows loading until config is set and loaded; supports error state and optional secure container.
|
|
6
4
|
@objc(RemoteUiView)
|
|
7
5
|
final class RemoteUiView: UIView {
|
|
8
6
|
|
|
7
|
+
// MARK: - URL handler for DivKit
|
|
8
|
+
private final class RemoteUiUrlHandler: DivUrlHandler {
|
|
9
|
+
weak var owner: RemoteUiView?
|
|
10
|
+
|
|
11
|
+
func handle(_ url: URL, info: DivActionInfo, sender: AnyObject?) {
|
|
12
|
+
guard let owner else { return }
|
|
13
|
+
|
|
14
|
+
if owner.isCustomAction(url) {
|
|
15
|
+
owner.handleDivAction(info) // send to RN
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Default behavior for normal links
|
|
20
|
+
if url.scheme?.lowercased() == "http" || url.scheme?.lowercased() == "https" {
|
|
21
|
+
DispatchQueue.main.async {
|
|
22
|
+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
// Other non-http(s) schemes: either treat as custom or try open
|
|
26
|
+
DispatchQueue.main.async {
|
|
27
|
+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Backward compatible overload (DivKit may call this)
|
|
33
|
+
func handle(_ url: URL, sender: AnyObject?) {
|
|
34
|
+
// If DivKit calls the short version, we still try to open it
|
|
35
|
+
DispatchQueue.main.async {
|
|
36
|
+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
private let divView: DivView
|
|
10
|
-
private
|
|
42
|
+
private let divKitComponents: DivKitComponents
|
|
43
|
+
private let urlHandler: RemoteUiUrlHandler
|
|
44
|
+
|
|
11
45
|
private var currentCardId: String = "pnlight_card"
|
|
12
46
|
|
|
13
47
|
private let loadingIndicator: UIActivityIndicatorView
|
|
14
48
|
private let errorLabel: UILabel
|
|
15
49
|
private var secureContainer: UIView?
|
|
16
50
|
|
|
51
|
+
@objc var onAction: (([String: Any]) -> Void)?
|
|
52
|
+
|
|
17
53
|
override init(frame: CGRect) {
|
|
18
|
-
self.divView = DivView(divKitComponents: Self.divKitComponents)
|
|
19
54
|
self.loadingIndicator = UIActivityIndicatorView(style: .large)
|
|
20
55
|
self.errorLabel = UILabel()
|
|
21
56
|
|
|
57
|
+
// Create url handler first (needed for DivKitComponents init)
|
|
58
|
+
self.urlHandler = RemoteUiUrlHandler()
|
|
59
|
+
|
|
60
|
+
// Pass urlHandler via initializer (actionHandler is a let constant inside DivKitComponents)
|
|
61
|
+
self.divKitComponents = DivKitComponents(urlHandler: urlHandler)
|
|
62
|
+
self.divView = DivView(divKitComponents: divKitComponents)
|
|
63
|
+
|
|
22
64
|
super.init(frame: frame)
|
|
23
65
|
|
|
66
|
+
// bind back-reference
|
|
67
|
+
self.urlHandler.owner = self
|
|
68
|
+
|
|
24
69
|
backgroundColor = .clear
|
|
25
70
|
|
|
26
71
|
divView.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -52,12 +97,15 @@ final class RemoteUiView: UIView {
|
|
|
52
97
|
secure.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
53
98
|
secure.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
54
99
|
secure.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
100
|
+
|
|
55
101
|
divView.topAnchor.constraint(equalTo: secure.topAnchor),
|
|
56
102
|
divView.leadingAnchor.constraint(equalTo: secure.leadingAnchor),
|
|
57
103
|
divView.trailingAnchor.constraint(equalTo: secure.trailingAnchor),
|
|
58
104
|
divView.bottomAnchor.constraint(equalTo: secure.bottomAnchor),
|
|
105
|
+
|
|
59
106
|
loadingIndicator.centerXAnchor.constraint(equalTo: secure.centerXAnchor),
|
|
60
107
|
loadingIndicator.centerYAnchor.constraint(equalTo: secure.centerYAnchor),
|
|
108
|
+
|
|
61
109
|
errorLabel.topAnchor.constraint(equalTo: secure.topAnchor),
|
|
62
110
|
errorLabel.leadingAnchor.constraint(equalTo: secure.leadingAnchor),
|
|
63
111
|
errorLabel.trailingAnchor.constraint(equalTo: secure.trailingAnchor),
|
|
@@ -73,8 +121,10 @@ final class RemoteUiView: UIView {
|
|
|
73
121
|
divView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
74
122
|
divView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
75
123
|
divView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
124
|
+
|
|
76
125
|
loadingIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
77
126
|
loadingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
127
|
+
|
|
78
128
|
errorLabel.topAnchor.constraint(equalTo: topAnchor),
|
|
79
129
|
errorLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
80
130
|
errorLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
@@ -87,7 +137,6 @@ final class RemoteUiView: UIView {
|
|
|
87
137
|
fatalError("init(coder:) has not been implemented")
|
|
88
138
|
}
|
|
89
139
|
|
|
90
|
-
/// Creates a secure container that hides content during screenshots and screen recording.
|
|
91
140
|
private static func makeSecureContainer() -> UIView? {
|
|
92
141
|
let textField = UITextField()
|
|
93
142
|
textField.isSecureTextEntry = true
|
|
@@ -104,9 +153,8 @@ final class RemoteUiView: UIView {
|
|
|
104
153
|
return secureView
|
|
105
154
|
}
|
|
106
155
|
|
|
107
|
-
/// Renders the given DivKit JSON string. Pass nil to show blank (e.g. while loading).
|
|
108
156
|
private func applyConfig(configJson: String?, cardId: String) {
|
|
109
|
-
guard let configJson
|
|
157
|
+
guard let configJson, !configJson.isEmpty,
|
|
110
158
|
let jsonData = configJson.data(using: .utf8) else {
|
|
111
159
|
return
|
|
112
160
|
}
|
|
@@ -149,10 +197,41 @@ final class RemoteUiView: UIView {
|
|
|
149
197
|
}
|
|
150
198
|
}
|
|
151
199
|
|
|
152
|
-
|
|
200
|
+
private func isCustomAction(_ url: URL) -> Bool {
|
|
201
|
+
guard let scheme = url.scheme?.lowercased() else { return false }
|
|
202
|
+
return scheme != "http" && scheme != "https"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private func handleDivAction(_ action: DivActionInfo) {
|
|
206
|
+
guard let onAction else { return }
|
|
207
|
+
|
|
208
|
+
var payload: [String: Any] = [:]
|
|
209
|
+
|
|
210
|
+
if let url = action.url {
|
|
211
|
+
payload["url"] = url.absoluteString
|
|
212
|
+
|
|
213
|
+
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
|
|
214
|
+
payload["scheme"] = components.scheme ?? ""
|
|
215
|
+
payload["path"] = components.path
|
|
216
|
+
|
|
217
|
+
if let queryItems = components.queryItems {
|
|
218
|
+
var params: [String: String] = [:]
|
|
219
|
+
for item in queryItems {
|
|
220
|
+
params[item.name] = item.value ?? ""
|
|
221
|
+
}
|
|
222
|
+
payload["params"] = params
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
payload["logId"] = action.logId
|
|
228
|
+
|
|
229
|
+
onAction(payload)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// MARK: - React Native props
|
|
153
233
|
@objc func setConfig(_ value: NSString?) {
|
|
154
|
-
|
|
155
|
-
applyConfig(configJson: json, cardId: currentCardId)
|
|
234
|
+
applyConfig(configJson: value as String?, cardId: currentCardId)
|
|
156
235
|
}
|
|
157
236
|
|
|
158
237
|
@objc func setCardId(_ value: NSString?) {
|
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.2",
|
|
4
4
|
"description": "React Native wrapper for PNLight iOS binary SDK",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"PNLight.xcframework",
|
|
13
13
|
"PNLightSDK-ReactNative.podspec",
|
|
14
14
|
"RemoteUiView.js",
|
|
15
|
+
"RemoteUiView.d.ts",
|
|
15
16
|
"app.plugin.js",
|
|
16
17
|
"plugin"
|
|
17
18
|
],
|