cloudx-react-native 3.10.0 → 3.11.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 +1 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/cloudxreactnative/CloudXAdEvents.kt +10 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXAdInfo.kt +40 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdView.kt +718 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXNativeAdViewManagerImpl.kt +119 -0
- package/android/src/main/java/com/cloudxreactnative/CloudXPackage.kt +4 -1
- package/android/src/main/res/layout/cloudx_rn_native_media_banner_template.xml +100 -0
- package/android/src/main/res/layout/cloudx_rn_native_medium_template.xml +99 -0
- package/android/src/main/res/layout/cloudx_rn_native_small_template.xml +72 -0
- package/android/src/main/res/layout/cloudx_rn_native_vertical_media_template.xml +117 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/newarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +86 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXModule.kt +1 -0
- package/android/src/oldarch/com/cloudxreactnative/CloudXNativeAdViewManager.kt +118 -0
- package/cloudx-react-native.podspec +1 -1
- package/ios/CloudX.h +8 -0
- package/ios/CloudX.mm +159 -29
- package/ios/CloudXNativeAdView.h +75 -0
- package/ios/CloudXNativeAdView.mm +979 -0
- package/ios/CloudXNativeAdViewManager.h +12 -0
- package/ios/CloudXNativeAdViewManager.mm +119 -0
- package/lib/commonjs/CloudXAdViewBase.js +8 -97
- package/lib/commonjs/CloudXAdViewBase.js.map +1 -1
- package/lib/commonjs/CloudXNativeAdView.js +331 -0
- package/lib/commonjs/CloudXNativeAdView.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js +252 -0
- package/lib/commonjs/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js +117 -0
- package/lib/commonjs/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/commonjs/extraParameters.js +52 -0
- package/lib/commonjs/extraParameters.js.map +1 -0
- package/lib/commonjs/hooks.js +34 -0
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +72 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js +41 -0
- package/lib/commonjs/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/commonjs/types/NativeAd.js +70 -0
- package/lib/commonjs/types/NativeAd.js.map +1 -0
- package/lib/commonjs/types/index.js +13 -0
- package/lib/commonjs/types/index.js.map +1 -1
- package/lib/commonjs/useCloudXSdkInitialization.js +75 -0
- package/lib/commonjs/useCloudXSdkInitialization.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/module/CloudXAdViewBase.js +7 -96
- package/lib/module/CloudXAdViewBase.js.map +1 -1
- package/lib/module/CloudXNativeAdView.js +324 -0
- package/lib/module/CloudXNativeAdView.js.map +1 -0
- package/lib/module/CloudXNativeAdViewComponents.js +240 -0
- package/lib/module/CloudXNativeAdViewComponents.js.map +1 -0
- package/lib/module/CloudXNativeAdViewProvider.js +110 -0
- package/lib/module/CloudXNativeAdViewProvider.js.map +1 -0
- package/lib/module/extraParameters.js +45 -0
- package/lib/module/extraParameters.js.map +1 -0
- package/lib/module/hooks.js +34 -0
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js +35 -0
- package/lib/module/specs/CloudXNativeAdViewNativeComponent.js.map +1 -0
- package/lib/module/types/NativeAd.js +68 -0
- package/lib/module/types/NativeAd.js.map +1 -0
- package/lib/module/types/index.js +1 -0
- package/lib/module/types/index.js.map +1 -1
- package/lib/module/useCloudXSdkInitialization.js +68 -0
- package/lib/module/useCloudXSdkInitialization.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/CloudXAdViewBase.d.ts.map +1 -1
- package/lib/typescript/src/CloudXNativeAdView.d.ts +24 -0
- package/lib/typescript/src/CloudXNativeAdView.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts +67 -0
- package/lib/typescript/src/CloudXNativeAdViewComponents.d.ts.map +1 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts +72 -0
- package/lib/typescript/src/CloudXNativeAdViewProvider.d.ts.map +1 -0
- package/lib/typescript/src/extraParameters.d.ts +4 -0
- package/lib/typescript/src/extraParameters.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts +103 -0
- package/lib/typescript/src/specs/CloudXNativeAdViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types/AdInfo.d.ts +4 -1
- package/lib/typescript/src/types/AdInfo.d.ts.map +1 -1
- package/lib/typescript/src/types/NativeAd.d.ts +114 -0
- package/lib/typescript/src/types/NativeAd.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +2 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts +16 -0
- package/lib/typescript/src/useCloudXSdkInitialization.d.ts.map +1 -0
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/CloudXAdViewBase.tsx +8 -125
- package/src/CloudXNativeAdView.tsx +419 -0
- package/src/CloudXNativeAdViewComponents.tsx +203 -0
- package/src/CloudXNativeAdViewProvider.tsx +149 -0
- package/src/extraParameters.ts +65 -0
- package/src/hooks.ts +38 -0
- package/src/index.ts +24 -0
- package/src/specs/CloudXNativeAdViewNativeComponent.ts +137 -0
- package/src/types/AdInfo.ts +5 -1
- package/src/types/NativeAd.ts +125 -0
- package/src/types/index.ts +7 -0
- package/src/useCloudXSdkInitialization.ts +73 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react';
|
|
3
|
+
import { findNodeHandle, View } from 'react-native';
|
|
4
|
+
import type { NativeSyntheticEvent } from 'react-native';
|
|
5
|
+
import NativeCloudXNativeAdView, { Commands } from './specs/CloudXNativeAdViewNativeComponent';
|
|
6
|
+
import type { CloudXNativeAdInfoEvent, CloudXNativeAdLoadFailedEvent } from './specs/CloudXNativeAdViewNativeComponent';
|
|
7
|
+
import { toNativeExtraParameterEntries } from './extraParameters';
|
|
8
|
+
import { CloudXNativeAssetType } from './types/NativeAd';
|
|
9
|
+
import type { CloudXNativeAdMetadata, CloudXNativeAdViewHandle, CloudXNativeAdViewProps } from './types/NativeAd';
|
|
10
|
+
import type { CloudXAdDisplayFailedInfo, CloudXAdInfo, CloudXAdLoadFailedInfo } from './types/AdInfo';
|
|
11
|
+
import type { CloudXErrorCode } from './CloudXErrorCode';
|
|
12
|
+
import { CloudXNativeAdViewContext, CloudXNativeAdViewProvider } from './CloudXNativeAdViewProvider';
|
|
13
|
+
import { useCloudXSdkInitializationGate } from './useCloudXSdkInitialization';
|
|
14
|
+
|
|
15
|
+
type NativeViewRef = React.ElementRef<typeof NativeCloudXNativeAdView>;
|
|
16
|
+
|
|
17
|
+
const parseAdValues = (json?: string | null): Record<string, string> => {
|
|
18
|
+
if (!json) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(json);
|
|
23
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const parseNativeAdMetadata = (json?: string | null): CloudXNativeAdMetadata | null => {
|
|
30
|
+
if (!json) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(json);
|
|
35
|
+
return parsed && typeof parsed === 'object' ? (parsed as CloudXNativeAdMetadata) : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const toAdInfo = (event: CloudXNativeAdInfoEvent): CloudXAdInfo => {
|
|
42
|
+
const nativeAd = parseNativeAdMetadata(event.nativeAdJson);
|
|
43
|
+
return {
|
|
44
|
+
adUnitId: event.adUnitId,
|
|
45
|
+
adFormat: event.adFormat,
|
|
46
|
+
networkName: event.networkName,
|
|
47
|
+
networkPlacement: event.networkPlacement || null,
|
|
48
|
+
placement: event.placement || null,
|
|
49
|
+
creativeId: event.creativeId || null,
|
|
50
|
+
revenue: event.revenue ?? 0,
|
|
51
|
+
adValues: parseAdValues(event.adValuesJson),
|
|
52
|
+
nativeAd,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const toLoadFailedInfo = (event: CloudXNativeAdLoadFailedEvent): CloudXAdLoadFailedInfo => ({
|
|
57
|
+
adUnitId: event.adUnitId,
|
|
58
|
+
code: event.code as CloudXErrorCode,
|
|
59
|
+
message: event.message || null,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Renders a native ad. Defaults to the SDK's bundled template layouts;
|
|
64
|
+
* set `renderMode="manual"` to drive layout through the pre-built asset
|
|
65
|
+
* components (`CloudXTitleView`, `CloudXMediaView`, etc.) instead.
|
|
66
|
+
*
|
|
67
|
+
* Manual mode is never inferred from the presence of children — `renderMode`
|
|
68
|
+
* is always explicit so a publisher's intent is visible at the call site.
|
|
69
|
+
*/
|
|
70
|
+
export const CloudXNativeAdView = forwardRef<CloudXNativeAdViewHandle, CloudXNativeAdViewProps>(
|
|
71
|
+
function CloudXNativeAdView(props, ref) {
|
|
72
|
+
const { renderMode = 'template', style, testID, children } = props;
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Gate native ad-view creation on SDK initialization; shared with
|
|
76
|
+
* CloudXAdViewBase (banner/MREC) via useCloudXSdkInitializationGate.
|
|
77
|
+
* See that hook for why polling (rather than an init event) is
|
|
78
|
+
* required.
|
|
79
|
+
*/
|
|
80
|
+
const isInitialized = useCloudXSdkInitializationGate('CloudXNativeAdView');
|
|
81
|
+
|
|
82
|
+
// Dev-only guard: an asset component (CloudXTitleView, etc.) mounted
|
|
83
|
+
// while renderMode is 'template' silently does nothing on the native
|
|
84
|
+
// side, which is almost always a publisher forgetting to set
|
|
85
|
+
// renderMode="manual". Warn once per host instance.
|
|
86
|
+
const warnedAssetInTemplateMode = useRef(false);
|
|
87
|
+
const handleAssetRoleRegistered = useCallback(
|
|
88
|
+
(role: string) => {
|
|
89
|
+
if (renderMode !== 'template' || warnedAssetInTemplateMode.current) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
warnedAssetInTemplateMode.current = true;
|
|
93
|
+
console.warn(
|
|
94
|
+
`CloudXNativeAdView: <${role}/> is a manual-render asset component, but this ` +
|
|
95
|
+
'CloudXNativeAdView has renderMode="template" (the default). Set renderMode="manual" ' +
|
|
96
|
+
'to bind asset views, or remove the child to use the bundled template layout.'
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
[renderMode]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!isInitialized) {
|
|
103
|
+
// Preserve children + view-safe props so a manual layout's asset
|
|
104
|
+
// views mount and reserve their space before the SDK is ready.
|
|
105
|
+
// The imperative ref is NOT forwarded here — it stays unset until
|
|
106
|
+
// the native view mounts, so callers can't dispatch
|
|
107
|
+
// loadAd/destroyAd at a placeholder.
|
|
108
|
+
return (
|
|
109
|
+
<CloudXNativeAdViewProvider onAssetRoleRegistered={handleAssetRoleRegistered}>
|
|
110
|
+
<View style={style} testID={testID}>
|
|
111
|
+
{children}
|
|
112
|
+
</View>
|
|
113
|
+
</CloudXNativeAdViewProvider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<CloudXNativeAdViewProvider onAssetRoleRegistered={handleAssetRoleRegistered}>
|
|
119
|
+
<CloudXNativeAdViewImpl {...props} ref={ref} />
|
|
120
|
+
</CloudXNativeAdViewProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export default CloudXNativeAdView;
|
|
126
|
+
|
|
127
|
+
const CloudXNativeAdViewImpl = forwardRef<CloudXNativeAdViewHandle, CloudXNativeAdViewProps>(
|
|
128
|
+
function CloudXNativeAdViewImpl(props, ref) {
|
|
129
|
+
const {
|
|
130
|
+
adUnitId,
|
|
131
|
+
placement,
|
|
132
|
+
customData,
|
|
133
|
+
template,
|
|
134
|
+
disableVideoFullScreen,
|
|
135
|
+
startVideoUnmuted,
|
|
136
|
+
hideVideoMediaControls,
|
|
137
|
+
extraParameters,
|
|
138
|
+
localExtraParameters,
|
|
139
|
+
renderMode = 'template',
|
|
140
|
+
onAdLoaded,
|
|
141
|
+
onAdLoadFailed,
|
|
142
|
+
onAdDisplayFailed,
|
|
143
|
+
onAdClicked,
|
|
144
|
+
onAdExpired,
|
|
145
|
+
onAdClosed,
|
|
146
|
+
onAdRevenuePaid,
|
|
147
|
+
style,
|
|
148
|
+
testID,
|
|
149
|
+
children,
|
|
150
|
+
...rest
|
|
151
|
+
} = props;
|
|
152
|
+
|
|
153
|
+
const isManual = renderMode === 'manual';
|
|
154
|
+
|
|
155
|
+
const {
|
|
156
|
+
setNativeAd,
|
|
157
|
+
titleRef,
|
|
158
|
+
advertiserRef,
|
|
159
|
+
bodyRef,
|
|
160
|
+
callToActionRef,
|
|
161
|
+
iconRef,
|
|
162
|
+
optionsRef,
|
|
163
|
+
mediaRef,
|
|
164
|
+
starRatingRef,
|
|
165
|
+
mountedAssetRoles,
|
|
166
|
+
} = useContext(CloudXNativeAdViewContext);
|
|
167
|
+
|
|
168
|
+
const nativeRef = useRef<NativeViewRef | null>(null);
|
|
169
|
+
const pendingMetadata = useRef<CloudXNativeAdMetadata | null>(null);
|
|
170
|
+
// Cache of the last successfully loaded ad, used to backfill the
|
|
171
|
+
// CloudXAdInfo fields onAdDisplayFailedEvent doesn't carry (the
|
|
172
|
+
// native event is a bare {adUnitId, code, message} — a display
|
|
173
|
+
// failure always follows a prior load of the same ad).
|
|
174
|
+
const lastAdInfoRef = useRef<CloudXAdInfo | null>(null);
|
|
175
|
+
const [bindTick, setBindTick] = useState(0);
|
|
176
|
+
|
|
177
|
+
const loadAd = useCallback(() => {
|
|
178
|
+
if (nativeRef.current) {
|
|
179
|
+
Commands.loadAd(nativeRef.current);
|
|
180
|
+
}
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const destroyAd = useCallback(() => {
|
|
184
|
+
if (nativeRef.current) {
|
|
185
|
+
Commands.destroyAd(nativeRef.current);
|
|
186
|
+
}
|
|
187
|
+
// Clear the shared metadata so asset components stop rendering the
|
|
188
|
+
// destroyed ad's content and a subsequent load starts clean.
|
|
189
|
+
pendingMetadata.current = null;
|
|
190
|
+
lastAdInfoRef.current = null;
|
|
191
|
+
setNativeAd(null);
|
|
192
|
+
}, [setNativeAd]);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Escape hatch for imperative control. The native view auto-loads on
|
|
196
|
+
* attach and tears down its native ad on unmount; most integrations
|
|
197
|
+
* never need this ref. Primary lifecycle is still mount/props/unmount.
|
|
198
|
+
*
|
|
199
|
+
* `loadAd()` does NOT force a new auction. Both native SDKs re-serve the
|
|
200
|
+
* ad they already hold while it is neither expired nor destroyed —
|
|
201
|
+
* including after its impression — so changed targeting is not picked
|
|
202
|
+
* up. Call `destroyAd()` first to get a genuinely fresh ad.
|
|
203
|
+
*/
|
|
204
|
+
useImperativeHandle(ref, () => ({ loadAd, destroyAd }), [loadAd, destroyAd]);
|
|
205
|
+
|
|
206
|
+
const nativeExtraParameters = useMemo(() => toNativeExtraParameterEntries(extraParameters), [extraParameters]);
|
|
207
|
+
const nativeLocalExtraParameters = useMemo(
|
|
208
|
+
() => toNativeExtraParameterEntries(localExtraParameters),
|
|
209
|
+
[localExtraParameters]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const bindAssetView = useCallback((assetRef: React.RefObject<unknown> | undefined, assetName: CloudXNativeAssetType): boolean => {
|
|
213
|
+
if (!nativeRef.current || !assetRef?.current) return false;
|
|
214
|
+
const assetTag = findNodeHandle(assetRef.current as React.Component | null);
|
|
215
|
+
if (assetTag == null) return false;
|
|
216
|
+
Commands.updateAssetView(nativeRef.current, assetTag, assetName);
|
|
217
|
+
return true;
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
const handleAdLoaded = useCallback(
|
|
221
|
+
(event: NativeSyntheticEvent<CloudXNativeAdInfoEvent>) => {
|
|
222
|
+
const adInfo = toAdInfo(event.nativeEvent);
|
|
223
|
+
lastAdInfoRef.current = adInfo;
|
|
224
|
+
|
|
225
|
+
if (isManual && adInfo.nativeAd) {
|
|
226
|
+
pendingMetadata.current = adInfo.nativeAd;
|
|
227
|
+
setNativeAd(adInfo.nativeAd);
|
|
228
|
+
setBindTick((n) => n + 1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
onAdLoaded?.(adInfo);
|
|
232
|
+
},
|
|
233
|
+
[isManual, setNativeAd, onAdLoaded]
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const handleAdLoadFailed = useCallback(
|
|
237
|
+
(event: NativeSyntheticEvent<CloudXNativeAdLoadFailedEvent>) => {
|
|
238
|
+
onAdLoadFailed?.(toLoadFailedInfo(event.nativeEvent));
|
|
239
|
+
},
|
|
240
|
+
[onAdLoadFailed]
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const handleAdDisplayFailed = useCallback(
|
|
244
|
+
(event: NativeSyntheticEvent<CloudXNativeAdLoadFailedEvent>) => {
|
|
245
|
+
if (!onAdDisplayFailed) return;
|
|
246
|
+
const { adUnitId: failedAdUnitId, code, message } = event.nativeEvent;
|
|
247
|
+
// The cached ad info is only a valid description of this
|
|
248
|
+
// failure if it describes the ad unit that actually failed. A
|
|
249
|
+
// display failure arriving after a re-target would otherwise
|
|
250
|
+
// report the previous unit's network, placement and revenue.
|
|
251
|
+
const cached = lastAdInfoRef.current;
|
|
252
|
+
const base = cached && cached.adUnitId === failedAdUnitId ? cached : null;
|
|
253
|
+
const displayFailedInfo: CloudXAdDisplayFailedInfo = {
|
|
254
|
+
// The event's own adUnitId is authoritative.
|
|
255
|
+
adUnitId: failedAdUnitId || base?.adUnitId || '',
|
|
256
|
+
adFormat: base?.adFormat ?? 'NATIVE',
|
|
257
|
+
networkName: base?.networkName ?? '',
|
|
258
|
+
networkPlacement: base?.networkPlacement ?? null,
|
|
259
|
+
placement: base?.placement ?? null,
|
|
260
|
+
revenue: base?.revenue ?? 0,
|
|
261
|
+
adValues: base?.adValues ?? {},
|
|
262
|
+
nativeAd: base?.nativeAd ?? null,
|
|
263
|
+
code: code as CloudXErrorCode,
|
|
264
|
+
message: message || null,
|
|
265
|
+
};
|
|
266
|
+
onAdDisplayFailed(displayFailedInfo);
|
|
267
|
+
},
|
|
268
|
+
[onAdDisplayFailed]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const handleAdClicked = useCallback(
|
|
272
|
+
(event: NativeSyntheticEvent<CloudXNativeAdInfoEvent>) => onAdClicked?.(toAdInfo(event.nativeEvent)),
|
|
273
|
+
[onAdClicked]
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const handleAdExpired = useCallback(
|
|
277
|
+
(event: NativeSyntheticEvent<CloudXNativeAdInfoEvent>) => onAdExpired?.(toAdInfo(event.nativeEvent)),
|
|
278
|
+
[onAdExpired]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const handleAdClosed = useCallback(
|
|
282
|
+
(event: NativeSyntheticEvent<CloudXNativeAdInfoEvent>) => onAdClosed?.(toAdInfo(event.nativeEvent)),
|
|
283
|
+
[onAdClosed]
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const handleAdRevenuePaid = useCallback(
|
|
287
|
+
(event: NativeSyntheticEvent<CloudXNativeAdInfoEvent>) => onAdRevenuePaid?.(toAdInfo(event.nativeEvent)),
|
|
288
|
+
[onAdRevenuePaid]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!isManual || bindTick === 0) return;
|
|
293
|
+
const metadata = pendingMetadata.current;
|
|
294
|
+
if (!metadata) return;
|
|
295
|
+
|
|
296
|
+
const iconAvailable = metadata.isIconImageAvailable;
|
|
297
|
+
const plan: Array<[React.RefObject<unknown> | undefined, CloudXNativeAssetType, boolean]> = [
|
|
298
|
+
[titleRef, CloudXNativeAssetType.TITLE, Boolean(metadata.title)],
|
|
299
|
+
[advertiserRef, CloudXNativeAssetType.ADVERTISER, Boolean(metadata.advertiser)],
|
|
300
|
+
[bodyRef, CloudXNativeAssetType.BODY, Boolean(metadata.body)],
|
|
301
|
+
[callToActionRef, CloudXNativeAssetType.CALL_TO_ACTION, Boolean(metadata.callToAction)],
|
|
302
|
+
[iconRef, CloudXNativeAssetType.ICON, iconAvailable],
|
|
303
|
+
[optionsRef, CloudXNativeAssetType.OPTIONS, metadata.isOptionsViewAvailable],
|
|
304
|
+
[mediaRef, CloudXNativeAssetType.MEDIA, metadata.isMediaViewAvailable],
|
|
305
|
+
[starRatingRef, CloudXNativeAssetType.STAR_RATING, metadata.starRating != null && metadata.starRating > 0],
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
// Asset refs mount asynchronously: components render from context
|
|
309
|
+
// state that was just set, so on the first frame after onAdLoaded
|
|
310
|
+
// some refs may still be null. Retry binding across a few frames
|
|
311
|
+
// and only dispatch renderNativeAd (which registers click +
|
|
312
|
+
// impression tracking) once every wanted asset is bound — or,
|
|
313
|
+
// after exhausting retries, render with what did bind and warn.
|
|
314
|
+
const MAX_BIND_ATTEMPTS = 10;
|
|
315
|
+
let cancelled = false;
|
|
316
|
+
// Track every retry timer so unmount can cancel the chain instead
|
|
317
|
+
// of leaving fire-and-forget timers that dispatch commands at a
|
|
318
|
+
// recycled or unmounted view tag.
|
|
319
|
+
const pendingTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
320
|
+
const scheduleRetry = (fn: () => void): void => {
|
|
321
|
+
const timer = setTimeout(fn, 16);
|
|
322
|
+
pendingTimers.add(timer);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const attemptBind = (attempt: number, remaining: Set<CloudXNativeAssetType>) => {
|
|
326
|
+
if (cancelled || pendingMetadata.current == null) return;
|
|
327
|
+
if (!nativeRef.current) {
|
|
328
|
+
if (attempt < MAX_BIND_ATTEMPTS) {
|
|
329
|
+
scheduleRetry(() => attemptBind(attempt + 1, remaining));
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const [assetRef, assetName, wanted] of plan) {
|
|
335
|
+
if (!wanted || !remaining.has(assetName)) continue;
|
|
336
|
+
if (bindAssetView(assetRef, assetName)) {
|
|
337
|
+
remaining.delete(assetName);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (remaining.size > 0 && attempt < MAX_BIND_ATTEMPTS) {
|
|
342
|
+
scheduleRetry(() => attemptBind(attempt + 1, remaining));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (remaining.size > 0) {
|
|
347
|
+
console.warn(
|
|
348
|
+
`CloudXNativeAdView: asset view(s) never mounted for binding: ${[...remaining].join(', ')}. ` +
|
|
349
|
+
'Rendering with the bound subset; click/impression tracking may be incomplete.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
Commands.renderNativeAd(nativeRef.current);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// An asset is only worth waiting for when the ad carries it AND the
|
|
356
|
+
// publisher mounted its component. Waiting on the ad's asset set
|
|
357
|
+
// alone made every layout that omits an optional component (an ad
|
|
358
|
+
// with an advertiser rendered by a layout with no
|
|
359
|
+
// CloudXAdvertiserView, say) burn all MAX_BIND_ATTEMPTS retries
|
|
360
|
+
// before dispatching renderNativeAd, delaying impression and click
|
|
361
|
+
// registration, and warn about it on every such ad.
|
|
362
|
+
const wanted = new Set(
|
|
363
|
+
plan
|
|
364
|
+
.filter(([, name, w]) => w && mountedAssetRoles.current.has(`CloudX${name}`))
|
|
365
|
+
.map(([, name]) => name)
|
|
366
|
+
);
|
|
367
|
+
attemptBind(1, wanted);
|
|
368
|
+
|
|
369
|
+
return () => {
|
|
370
|
+
cancelled = true;
|
|
371
|
+
pendingTimers.forEach((timer) => clearTimeout(timer));
|
|
372
|
+
pendingTimers.clear();
|
|
373
|
+
};
|
|
374
|
+
}, [
|
|
375
|
+
bindTick,
|
|
376
|
+
isManual,
|
|
377
|
+
bindAssetView,
|
|
378
|
+
titleRef,
|
|
379
|
+
advertiserRef,
|
|
380
|
+
bodyRef,
|
|
381
|
+
callToActionRef,
|
|
382
|
+
iconRef,
|
|
383
|
+
optionsRef,
|
|
384
|
+
mediaRef,
|
|
385
|
+
starRatingRef,
|
|
386
|
+
mountedAssetRoles,
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<NativeCloudXNativeAdView
|
|
391
|
+
{...rest}
|
|
392
|
+
ref={(r) => {
|
|
393
|
+
nativeRef.current = r;
|
|
394
|
+
}}
|
|
395
|
+
adUnitId={adUnitId}
|
|
396
|
+
templateName={template}
|
|
397
|
+
placement={placement ?? null}
|
|
398
|
+
customData={customData ?? null}
|
|
399
|
+
disableVideoFullScreen={disableVideoFullScreen}
|
|
400
|
+
startVideoUnmuted={startVideoUnmuted}
|
|
401
|
+
hideVideoMediaControls={hideVideoMediaControls}
|
|
402
|
+
extraParameters={nativeExtraParameters}
|
|
403
|
+
localExtraParameters={nativeLocalExtraParameters}
|
|
404
|
+
manualRender={isManual}
|
|
405
|
+
onAdLoadedEvent={handleAdLoaded}
|
|
406
|
+
onAdLoadFailedEvent={handleAdLoadFailed}
|
|
407
|
+
onAdDisplayFailedEvent={handleAdDisplayFailed}
|
|
408
|
+
onAdClickedEvent={handleAdClicked}
|
|
409
|
+
onAdExpiredEvent={handleAdExpired}
|
|
410
|
+
onAdClosedEvent={handleAdClosed}
|
|
411
|
+
onAdRevenuePaidEvent={handleAdRevenuePaid}
|
|
412
|
+
style={style}
|
|
413
|
+
testID={testID}
|
|
414
|
+
>
|
|
415
|
+
{children}
|
|
416
|
+
</NativeCloudXNativeAdView>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built asset components for the manual-render `<CloudXNativeAdView/>`
|
|
3
|
+
* flow. Each component reads its content + ref from
|
|
4
|
+
* @{link CloudXNativeAdViewContext} and renders a minimal RN primitive so the
|
|
5
|
+
* publisher can style it with `style` / `textStyle` props without reaching
|
|
6
|
+
* into SDK internals.
|
|
7
|
+
*
|
|
8
|
+
* Mount these as descendants of a `<CloudXNativeAdView/>`. A component
|
|
9
|
+
* mounted outside the provider renders empty rather than throwing, so
|
|
10
|
+
* publisher layouts can be shown before the first ad loads. Each component
|
|
11
|
+
* calls `registerAssetRole` on mount so the host view can warn if it is used
|
|
12
|
+
* while `renderMode` is `'template'` (see CloudXNativeAdViewProvider).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { useContext, useEffect, useMemo } from 'react';
|
|
17
|
+
import { Image, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
18
|
+
import type { ImageProps, TextProps, ViewProps } from 'react-native';
|
|
19
|
+
import { CloudXNativeAdViewContext } from './CloudXNativeAdViewProvider';
|
|
20
|
+
|
|
21
|
+
// React Native's legacy component types expect `Ref<T>` (T is non-null),
|
|
22
|
+
// while `useRef<T | null>(null)` yields `RefObject<T | null>`. The shapes
|
|
23
|
+
// are compatible at runtime; the cast documents that we are intentionally
|
|
24
|
+
// bridging the two.
|
|
25
|
+
type TextRefLegacy = React.Ref<Text>;
|
|
26
|
+
type ImageRefLegacy = React.Ref<Image>;
|
|
27
|
+
type ViewRefLegacy = React.Ref<View>;
|
|
28
|
+
|
|
29
|
+
/** Renders the headline. */
|
|
30
|
+
export const CloudXTitleView: React.FC<TextProps> = (props) => {
|
|
31
|
+
const { titleRef, nativeAd, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
32
|
+
useEffect(() => registerAssetRole('CloudXTitleView'), [registerAssetRole]);
|
|
33
|
+
return (
|
|
34
|
+
<Text {...props} ref={titleRef as TextRefLegacy}>
|
|
35
|
+
{nativeAd?.title ?? ''}
|
|
36
|
+
</Text>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Renders the advertiser / sponsor name. */
|
|
41
|
+
export const CloudXAdvertiserView: React.FC<TextProps> = (props) => {
|
|
42
|
+
const { advertiserRef, nativeAd, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
43
|
+
useEffect(() => registerAssetRole('CloudXAdvertiserView'), [registerAssetRole]);
|
|
44
|
+
return (
|
|
45
|
+
<Text {...props} ref={advertiserRef as TextRefLegacy}>
|
|
46
|
+
{nativeAd?.advertiser ?? ''}
|
|
47
|
+
</Text>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Renders the body / description copy. */
|
|
52
|
+
export const CloudXBodyView: React.FC<TextProps> = (props) => {
|
|
53
|
+
const { bodyRef, nativeAd, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
54
|
+
useEffect(() => registerAssetRole('CloudXBodyView'), [registerAssetRole]);
|
|
55
|
+
return (
|
|
56
|
+
<Text {...props} ref={bodyRef as TextRefLegacy}>
|
|
57
|
+
{nativeAd?.body ?? ''}
|
|
58
|
+
</Text>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Renders the call-to-action label.
|
|
64
|
+
*
|
|
65
|
+
* iOS wraps the Text in a TouchableOpacity so publishers get the standard
|
|
66
|
+
* press-feedback animation; the Text ref is what the native bridge tags,
|
|
67
|
+
* and UIKit's touch dispatch lets the SDK's gesture recognizer run.
|
|
68
|
+
*
|
|
69
|
+
* Android renders a bare Text. RN's gesture responder system treats a
|
|
70
|
+
* wrapping TouchableOpacity as a touch-capturing ancestor, which blocks
|
|
71
|
+
* the OnClickListener the 3P SDK installs on the Text when registering
|
|
72
|
+
* it for click handling. Without the wrapper, the SDK-set listener
|
|
73
|
+
* receives the tap normally.
|
|
74
|
+
*/
|
|
75
|
+
export const CloudXCallToActionView: React.FC<TextProps> = (props) => {
|
|
76
|
+
const { callToActionRef, nativeAd, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
77
|
+
useEffect(() => registerAssetRole('CloudXCallToActionView'), [registerAssetRole]);
|
|
78
|
+
const label = nativeAd?.callToAction ?? '';
|
|
79
|
+
|
|
80
|
+
const content = (
|
|
81
|
+
<Text {...props} ref={callToActionRef as TextRefLegacy}>
|
|
82
|
+
{label}
|
|
83
|
+
</Text>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (Platform.OS === 'android') {
|
|
87
|
+
return content;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return <TouchableOpacity activeOpacity={0.8}>{content}</TouchableOpacity>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Renders the icon. Prefers the remote URL when available (fast CDN fetch),
|
|
95
|
+
* falling back to the base64-encoded bytes surfaced by the bridge for
|
|
96
|
+
* adapters that only ship a pre-loaded drawable.
|
|
97
|
+
*/
|
|
98
|
+
export const CloudXIconView: React.FC<Omit<ImageProps, 'source'>> = (props) => {
|
|
99
|
+
const { iconRef, nativeAd, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
100
|
+
useEffect(() => registerAssetRole('CloudXIconView'), [registerAssetRole]);
|
|
101
|
+
|
|
102
|
+
const source = useMemo(() => {
|
|
103
|
+
if (nativeAd?.url) return { uri: nativeAd.url };
|
|
104
|
+
if (nativeAd?.imageSource) return { uri: `data:image/png;base64,${nativeAd.imageSource}` };
|
|
105
|
+
return undefined;
|
|
106
|
+
}, [nativeAd?.url, nativeAd?.imageSource]);
|
|
107
|
+
|
|
108
|
+
if (!source) {
|
|
109
|
+
// Placeholder path: only forward View-safe props. Spreading ImageProps
|
|
110
|
+
// (resizeMode, fadeDuration, …) onto a <View/> triggers RN warnings.
|
|
111
|
+
const { style, testID } = props;
|
|
112
|
+
return <View style={style} testID={testID} ref={iconRef as unknown as ViewRefLegacy} />;
|
|
113
|
+
}
|
|
114
|
+
return <Image {...props} ref={iconRef as ImageRefLegacy} source={source} />;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Renders the ad-choices / options container. The adapter's actual options
|
|
119
|
+
* view is inserted as a native child of this element by the bridge.
|
|
120
|
+
*/
|
|
121
|
+
export const CloudXOptionsView: React.FC<ViewProps> = (props) => {
|
|
122
|
+
const { optionsRef, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
123
|
+
useEffect(() => registerAssetRole('CloudXOptionsView'), [registerAssetRole]);
|
|
124
|
+
// collapsable={false} keeps a real native view alive for the SDK to insert
|
|
125
|
+
// into. A layout-only style lets the renderer flatten this view away;
|
|
126
|
+
// findNodeHandle still returns a shadow tag, so binding reports success
|
|
127
|
+
// while the native lookup finds nothing and the asset is never inserted.
|
|
128
|
+
return <View {...props} collapsable={false} ref={optionsRef as ViewRefLegacy} />;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Renders the media container. The adapter's media view (static image or
|
|
133
|
+
* video surface) is inserted as a native child of this element by the
|
|
134
|
+
* bridge.
|
|
135
|
+
*/
|
|
136
|
+
export const CloudXMediaView: React.FC<ViewProps> = (props) => {
|
|
137
|
+
const { mediaRef, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
138
|
+
useEffect(() => registerAssetRole('CloudXMediaView'), [registerAssetRole]);
|
|
139
|
+
// collapsable={false} keeps a real native view alive for the SDK to insert
|
|
140
|
+
// into. A layout-only style lets the renderer flatten this view away;
|
|
141
|
+
// findNodeHandle still returns a shadow tag, so binding reports success
|
|
142
|
+
// while the native lookup finds nothing and the asset is never inserted.
|
|
143
|
+
return <View {...props} collapsable={false} ref={mediaRef as ViewRefLegacy} />;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type StarRatingViewProps = ViewProps & {
|
|
147
|
+
/** Color for filled stars. Defaults to #ffe234. */
|
|
148
|
+
color?: string;
|
|
149
|
+
/** Color for empty stars. Defaults to #dedede. */
|
|
150
|
+
shadowColor?: string;
|
|
151
|
+
/** Star size in points. Defaults to 10. */
|
|
152
|
+
size?: number;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Renders the star rating using Unicode glyphs. Partial fractions are
|
|
157
|
+
* clipped so a 3.5-star rating displays as 3.5 stars.
|
|
158
|
+
*/
|
|
159
|
+
export const CloudXStarRatingView: React.FC<StarRatingViewProps> = ({ color = '#ffe234', shadowColor = '#dedede', size = 10, style }) => {
|
|
160
|
+
const { nativeAd, starRatingRef, registerAssetRole } = useContext(CloudXNativeAdViewContext);
|
|
161
|
+
useEffect(() => registerAssetRole('CloudXStarRatingView'), [registerAssetRole]);
|
|
162
|
+
const maxStars = 5;
|
|
163
|
+
|
|
164
|
+
const containerStyle = useMemo(() => StyleSheet.flatten([style, styles.starRatingContainer]), [style]);
|
|
165
|
+
|
|
166
|
+
const rating = Math.max(0, Math.min(maxStars, nativeAd?.starRating ?? 0));
|
|
167
|
+
|
|
168
|
+
if (!rating) {
|
|
169
|
+
return <View style={containerStyle} ref={starRatingRef as ViewRefLegacy} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const stars = Array.from({ length: maxStars }).map((_, index) => {
|
|
173
|
+
const isFull = rating > index;
|
|
174
|
+
const width = Math.min(size, Math.max(0, (rating - index) * size));
|
|
175
|
+
return (
|
|
176
|
+
<View key={index}>
|
|
177
|
+
<Text style={{ fontSize: size, color: isFull ? color : shadowColor }}>{String.fromCodePoint(0x2606)}</Text>
|
|
178
|
+
{isFull && (
|
|
179
|
+
<View style={[{ width }, styles.starRatingOverlay]}>
|
|
180
|
+
<Text style={{ fontSize: size, color }}>{String.fromCodePoint(0x2605)}</Text>
|
|
181
|
+
</View>
|
|
182
|
+
)}
|
|
183
|
+
</View>
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<View style={containerStyle} ref={starRatingRef as ViewRefLegacy}>
|
|
189
|
+
{stars}
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const styles = StyleSheet.create({
|
|
195
|
+
starRatingContainer: {
|
|
196
|
+
flexDirection: 'row',
|
|
197
|
+
alignItems: 'center',
|
|
198
|
+
},
|
|
199
|
+
starRatingOverlay: {
|
|
200
|
+
overflow: 'hidden',
|
|
201
|
+
position: 'absolute',
|
|
202
|
+
},
|
|
203
|
+
});
|