@media-suite/reactnative-sdk 0.1.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 +120 -0
- package/dist/MediaSuiteAd.d.ts +12 -0
- package/dist/MediaSuiteAd.js +159 -0
- package/dist/MediaSuiteProvider.d.ts +16 -0
- package/dist/MediaSuiteProvider.js +126 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +25 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @mediasuite/react-native-sdk
|
|
2
|
+
|
|
3
|
+
React Native SDK for MediaSuite ad platform. Renders ads, tracks impressions/clicks with IAB/MRC viewability compliance.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mediasuite/react-native-sdk @react-native-async-storage/async-storage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Wrap your app with the Provider
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { MediaSuiteProvider } from '@mediasuite/react-native-sdk';
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
return (
|
|
20
|
+
<MediaSuiteProvider
|
|
21
|
+
config={{
|
|
22
|
+
companyId: '507f1f77bcf86cd799439011',
|
|
23
|
+
apiKey: 'your-public-embed-key',
|
|
24
|
+
baseUrl: 'https://api.mediasuite.com', // optional
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<MyApp />
|
|
28
|
+
</MediaSuiteProvider>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> **Security:** The `apiKey` must be a restricted public embed key with read-only SDK permissions. Never use admin or secret keys in client-side code.
|
|
34
|
+
|
|
35
|
+
### 2. Render ads anywhere
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { MediaSuiteAd } from '@mediasuite/react-native-sdk';
|
|
39
|
+
|
|
40
|
+
function HomeScreen() {
|
|
41
|
+
return (
|
|
42
|
+
<View>
|
|
43
|
+
<Text>My App</Text>
|
|
44
|
+
|
|
45
|
+
<MediaSuiteAd
|
|
46
|
+
spaceCode="banner-home-principal"
|
|
47
|
+
screenName="HomeScreen"
|
|
48
|
+
style={{ height: 90, marginVertical: 16 }}
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<MediaSuiteAd
|
|
52
|
+
spaceCode="sidebar-top"
|
|
53
|
+
screenName="HomeScreen"
|
|
54
|
+
style={{ height: 250 }}
|
|
55
|
+
onAdLoaded={(space) => console.log('Ads loaded:', space.ads.length)}
|
|
56
|
+
onError={(err) => console.warn('Ad error:', err)}
|
|
57
|
+
/>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## MediaSuiteAd Props
|
|
64
|
+
|
|
65
|
+
| Prop | Type | Required | Description |
|
|
66
|
+
|------|------|----------|-------------|
|
|
67
|
+
| `spaceCode` | `string` | Yes | Ad space code from MediaSuite panel |
|
|
68
|
+
| `screenName` | `string` | No | Screen name for referrer analytics (e.g. `'HomeScreen'`) |
|
|
69
|
+
| `style` | `ViewStyle` | No | Container styles |
|
|
70
|
+
| `onAdLoaded` | `(space) => void` | No | Callback when ads are loaded |
|
|
71
|
+
| `onError` | `(error) => void` | No | Callback on error |
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
- **IAB/MRC Viewability** — 50% visible for 1 continuous second before counting impression
|
|
76
|
+
- **Ad Rotation** — Automatic rotation when space has multiple ads
|
|
77
|
+
- **Impression Tracking** — Debounced, deduplicated, viewability-gated, with retry on network failure
|
|
78
|
+
- **Click Tracking** — Fire-and-forget tracking + deep link to destination
|
|
79
|
+
- **Visitor Persistence** — Crypto-secure UUID stored in AsyncStorage across sessions
|
|
80
|
+
- **Background Handling** — Pauses tracking when app goes to background
|
|
81
|
+
- **Reactive Device Detection** — Updates on orientation and window size changes
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- React Native >= 0.72
|
|
86
|
+
- React >= 18
|
|
87
|
+
- @react-native-async-storage/async-storage >= 1.19
|
|
88
|
+
|
|
89
|
+
## Building
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run build # Compiles TypeScript to dist/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The package ships compiled JS + type declarations via the `dist/` directory.
|
|
96
|
+
|
|
97
|
+
## Architecture
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
MediaSuiteProvider (Context)
|
|
101
|
+
├── Manages config, visitor ID, device detection
|
|
102
|
+
├── Caches ad space responses
|
|
103
|
+
├── Retry logic for failed impression tracking
|
|
104
|
+
└── Provides: fetchSpace, trackImpression, getClickUrl
|
|
105
|
+
|
|
106
|
+
MediaSuiteAd (Component)
|
|
107
|
+
├── Fetches ads for a space code
|
|
108
|
+
├── Renders Image with touch handling
|
|
109
|
+
├── IAB viewability: measureInWindow + 200ms polling
|
|
110
|
+
├── Passes screenName as page_url for referrer metrics
|
|
111
|
+
└── Auto-rotates ads on configurable interval
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Endpoints Used
|
|
115
|
+
|
|
116
|
+
| Endpoint | Purpose |
|
|
117
|
+
|----------|---------|
|
|
118
|
+
| `GET /sdk/serve/:companyId` | Fetch ads for spaces |
|
|
119
|
+
| `GET /sdk/impressions` | Register viewable impression |
|
|
120
|
+
| `GET /sdk/click/:placementId` | Track click + redirect |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ViewStyle } from 'react-native';
|
|
2
|
+
import type { AdSpace } from './types';
|
|
3
|
+
interface Props {
|
|
4
|
+
spaceCode: string;
|
|
5
|
+
style?: ViewStyle;
|
|
6
|
+
/** Screen name for analytics (e.g. 'HomeScreen'). Sent as page_url for referrer metrics. */
|
|
7
|
+
screenName?: string;
|
|
8
|
+
onAdLoaded?: (space: AdSpace) => void;
|
|
9
|
+
onError?: (error: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function MediaSuiteAd({ spaceCode, style, screenName, onAdLoaded, onError }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState, useRef, useCallback } from 'react';
|
|
3
|
+
import { View, Image, TouchableOpacity, Linking, StyleSheet, Dimensions, AppState, } from 'react-native';
|
|
4
|
+
import { useMediaSuite } from './MediaSuiteProvider';
|
|
5
|
+
export function MediaSuiteAd({ spaceCode, style, screenName, onAdLoaded, onError }) {
|
|
6
|
+
const { fetchSpace, trackImpression, getClickUrl, visitorId } = useMediaSuite();
|
|
7
|
+
const [ads, setAds] = useState([]);
|
|
8
|
+
const [index, setIndex] = useState(0);
|
|
9
|
+
const [loaded, setLoaded] = useState(false);
|
|
10
|
+
const rotationMs = useRef(5000);
|
|
11
|
+
const impressionTracked = useRef(false);
|
|
12
|
+
const viewRef = useRef(null);
|
|
13
|
+
const viewabilityTimer = useRef(null);
|
|
14
|
+
const scrollCheckInterval = useRef(null);
|
|
15
|
+
const adsRef = useRef([]);
|
|
16
|
+
const consecutiveFailures = useRef(0);
|
|
17
|
+
// Keep adsRef in sync for use inside interval callbacks
|
|
18
|
+
useEffect(() => { adsRef.current = ads; }, [ads]);
|
|
19
|
+
// Fetch ads — skip until visitorId is ready to avoid false onError
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!visitorId)
|
|
22
|
+
return;
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
(async () => {
|
|
25
|
+
const space = await fetchSpace(spaceCode);
|
|
26
|
+
if (cancelled)
|
|
27
|
+
return;
|
|
28
|
+
if (!space || !space.ads.length) {
|
|
29
|
+
onError?.('No ads for space: ' + spaceCode);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
rotationMs.current = (space.rotation_seconds || 5) * 1000;
|
|
33
|
+
setAds(space.ads);
|
|
34
|
+
consecutiveFailures.current = 0;
|
|
35
|
+
onAdLoaded?.(space);
|
|
36
|
+
})();
|
|
37
|
+
return () => { cancelled = true; };
|
|
38
|
+
}, [spaceCode, fetchSpace, visitorId, onAdLoaded, onError]);
|
|
39
|
+
// IAB viewability: 50% visible for 1 continuous second
|
|
40
|
+
const cancelViewabilityCheck = useCallback(() => {
|
|
41
|
+
if (viewabilityTimer.current) {
|
|
42
|
+
clearTimeout(viewabilityTimer.current);
|
|
43
|
+
viewabilityTimer.current = null;
|
|
44
|
+
}
|
|
45
|
+
}, []);
|
|
46
|
+
// Rotation — uses adsRef to avoid stale closures
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (ads.length <= 1)
|
|
49
|
+
return;
|
|
50
|
+
const timer = setInterval(() => {
|
|
51
|
+
setIndex((prev) => (prev + 1) % adsRef.current.length);
|
|
52
|
+
setLoaded(false);
|
|
53
|
+
impressionTracked.current = false;
|
|
54
|
+
cancelViewabilityCheck();
|
|
55
|
+
}, rotationMs.current);
|
|
56
|
+
return () => clearInterval(timer);
|
|
57
|
+
}, [ads.length, cancelViewabilityCheck]);
|
|
58
|
+
const checkViewability = useCallback(() => {
|
|
59
|
+
// Snapshot the current ad before async measurement
|
|
60
|
+
const currentAd = ads[index];
|
|
61
|
+
if (impressionTracked.current || !viewRef.current || !currentAd)
|
|
62
|
+
return;
|
|
63
|
+
viewRef.current.measureInWindow((x, y, width, height) => {
|
|
64
|
+
if (!width || !height)
|
|
65
|
+
return;
|
|
66
|
+
const screen = Dimensions.get('window');
|
|
67
|
+
const visibleTop = Math.max(0, y);
|
|
68
|
+
const visibleBottom = Math.min(screen.height, y + height);
|
|
69
|
+
const visibleLeft = Math.max(0, x);
|
|
70
|
+
const visibleRight = Math.min(screen.width, x + width);
|
|
71
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
72
|
+
const visibleWidth = Math.max(0, visibleRight - visibleLeft);
|
|
73
|
+
const visibleArea = visibleWidth * visibleHeight;
|
|
74
|
+
const totalArea = width * height;
|
|
75
|
+
const visibleRatio = totalArea > 0 ? visibleArea / totalArea : 0;
|
|
76
|
+
if (visibleRatio >= 0.5) {
|
|
77
|
+
// 50% visible — start 1s timer if not already running
|
|
78
|
+
if (!viewabilityTimer.current) {
|
|
79
|
+
viewabilityTimer.current = setTimeout(() => {
|
|
80
|
+
if (!impressionTracked.current) {
|
|
81
|
+
impressionTracked.current = true;
|
|
82
|
+
trackImpression(currentAd.placement_id, screenName);
|
|
83
|
+
}
|
|
84
|
+
}, 1000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Less than 50% visible — cancel timer
|
|
89
|
+
cancelViewabilityCheck();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}, [ads, index, trackImpression, cancelViewabilityCheck, screenName]);
|
|
93
|
+
// Poll viewability every 200ms while image is loaded
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!loaded || !ads.length)
|
|
96
|
+
return;
|
|
97
|
+
checkViewability();
|
|
98
|
+
scrollCheckInterval.current = setInterval(checkViewability, 200);
|
|
99
|
+
return () => {
|
|
100
|
+
if (scrollCheckInterval.current)
|
|
101
|
+
clearInterval(scrollCheckInterval.current);
|
|
102
|
+
cancelViewabilityCheck();
|
|
103
|
+
};
|
|
104
|
+
}, [loaded, ads.length, checkViewability, cancelViewabilityCheck]);
|
|
105
|
+
// Pause when app goes to background
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const sub = AppState.addEventListener('change', (state) => {
|
|
108
|
+
if (state !== 'active') {
|
|
109
|
+
cancelViewabilityCheck();
|
|
110
|
+
}
|
|
111
|
+
else if (loaded && !impressionTracked.current) {
|
|
112
|
+
checkViewability();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return () => sub.remove();
|
|
116
|
+
}, [loaded, checkViewability, cancelViewabilityCheck]);
|
|
117
|
+
// Image failure — advance to next ad if available, prevent infinite loop
|
|
118
|
+
const handleImageError = useCallback(() => {
|
|
119
|
+
onError?.('Image failed: ' + ads[index]?.image_url);
|
|
120
|
+
consecutiveFailures.current++;
|
|
121
|
+
if (consecutiveFailures.current < ads.length && ads.length > 1) {
|
|
122
|
+
setIndex((prev) => (prev + 1) % ads.length);
|
|
123
|
+
setLoaded(false);
|
|
124
|
+
impressionTracked.current = false;
|
|
125
|
+
cancelViewabilityCheck();
|
|
126
|
+
}
|
|
127
|
+
}, [ads, index, onError, cancelViewabilityCheck]);
|
|
128
|
+
// Click handler — fire-and-forget tracking + open destination URL
|
|
129
|
+
const handlePress = useCallback(async () => {
|
|
130
|
+
const ad = ads[index];
|
|
131
|
+
if (!ad)
|
|
132
|
+
return;
|
|
133
|
+
const clickUrl = getClickUrl(ad.placement_id, screenName);
|
|
134
|
+
// Fire tracking call (don't wait for response)
|
|
135
|
+
fetch(clickUrl).catch(() => { });
|
|
136
|
+
// Open the destination URL directly
|
|
137
|
+
if (ad.click_url) {
|
|
138
|
+
await Linking.openURL(ad.click_url).catch(() => { });
|
|
139
|
+
}
|
|
140
|
+
}, [ads, index, getClickUrl, screenName]);
|
|
141
|
+
if (!ads.length)
|
|
142
|
+
return null;
|
|
143
|
+
const ad = ads[index];
|
|
144
|
+
return (_jsx(TouchableOpacity, { activeOpacity: 0.9, onPress: handlePress, style: [styles.container, style], accessibilityLabel: ad.alt_text || 'Advertisement', accessibilityRole: "link", children: _jsx(View, { ref: viewRef, collapsable: false, children: _jsx(Image, { source: { uri: ad.image_url }, style: [styles.image, loaded && styles.imageLoaded], resizeMode: "contain", onLoad: () => { setLoaded(true); consecutiveFailures.current = 0; }, onError: handleImageError }, ad.placement_id) }) }));
|
|
145
|
+
}
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
container: {
|
|
148
|
+
overflow: 'hidden',
|
|
149
|
+
backgroundColor: 'transparent',
|
|
150
|
+
},
|
|
151
|
+
image: {
|
|
152
|
+
width: '100%',
|
|
153
|
+
aspectRatio: 16 / 9,
|
|
154
|
+
opacity: 0,
|
|
155
|
+
},
|
|
156
|
+
imageLoaded: {
|
|
157
|
+
opacity: 1,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MediaSuiteConfig, AdSpace } from './types';
|
|
3
|
+
interface ContextValue {
|
|
4
|
+
config: MediaSuiteConfig;
|
|
5
|
+
visitorId: string;
|
|
6
|
+
device: 'web' | 'mobile';
|
|
7
|
+
fetchSpace: (spaceCode: string) => Promise<AdSpace | null>;
|
|
8
|
+
trackImpression: (placementId: string, screenName?: string) => void;
|
|
9
|
+
getClickUrl: (placementId: string, screenName?: string) => string;
|
|
10
|
+
}
|
|
11
|
+
export declare const useMediaSuite: () => ContextValue;
|
|
12
|
+
export declare function MediaSuiteProvider({ config, children, }: {
|
|
13
|
+
config: MediaSuiteConfig;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useState, useRef, useCallback } from 'react';
|
|
3
|
+
import { useWindowDimensions } from 'react-native';
|
|
4
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
5
|
+
function uuid() {
|
|
6
|
+
// Prefer crypto.randomUUID (available since RN 0.73+ with Hermes)
|
|
7
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
8
|
+
return crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
// Fallback: crypto.getRandomValues
|
|
11
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
12
|
+
const buf = new Uint8Array(16);
|
|
13
|
+
crypto.getRandomValues(buf);
|
|
14
|
+
buf[6] = (buf[6] & 0x0f) | 0x40; // version 4
|
|
15
|
+
buf[8] = (buf[8] & 0x3f) | 0x80; // variant 1
|
|
16
|
+
const hex = Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
17
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
18
|
+
}
|
|
19
|
+
// Last resort: Math.random
|
|
20
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
21
|
+
const r = (Math.random() * 16) | 0;
|
|
22
|
+
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const Ctx = createContext(null);
|
|
26
|
+
export const useMediaSuite = () => {
|
|
27
|
+
const ctx = useContext(Ctx);
|
|
28
|
+
if (!ctx)
|
|
29
|
+
throw new Error('Wrap your app with <MediaSuiteProvider>');
|
|
30
|
+
return ctx;
|
|
31
|
+
};
|
|
32
|
+
const MAX_RETRIES = 2;
|
|
33
|
+
const RETRY_DELAY = 3000;
|
|
34
|
+
export function MediaSuiteProvider({ config, children, }) {
|
|
35
|
+
const baseUrl = (config.baseUrl || 'https://api.mediasuite.com').replace(/\/$/, '');
|
|
36
|
+
const [visitorId, setVisitorId] = useState('');
|
|
37
|
+
const sentImpressions = useRef(new Set());
|
|
38
|
+
const pendingImpressions = useRef([]);
|
|
39
|
+
const debounceTimer = useRef(null);
|
|
40
|
+
const cache = useRef({});
|
|
41
|
+
const retryTimer = useRef(null);
|
|
42
|
+
const isMounted = useRef(true);
|
|
43
|
+
const flushImpressionsRef = useRef(null);
|
|
44
|
+
// Reactive device detection — updates on orientation/window changes
|
|
45
|
+
const { width } = useWindowDimensions();
|
|
46
|
+
const device = width <= 768 ? 'mobile' : 'web';
|
|
47
|
+
// Restore or create visitor_id
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
(async () => {
|
|
50
|
+
let vid = await AsyncStorage.getItem('_ms_vid');
|
|
51
|
+
if (!vid) {
|
|
52
|
+
vid = uuid();
|
|
53
|
+
await AsyncStorage.setItem('_ms_vid', vid);
|
|
54
|
+
}
|
|
55
|
+
setVisitorId(vid);
|
|
56
|
+
})();
|
|
57
|
+
}, []);
|
|
58
|
+
// Cleanup all timers on unmount
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
return () => {
|
|
61
|
+
isMounted.current = false;
|
|
62
|
+
if (debounceTimer.current)
|
|
63
|
+
clearTimeout(debounceTimer.current);
|
|
64
|
+
if (retryTimer.current)
|
|
65
|
+
clearTimeout(retryTimer.current);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
const fetchSpace = useCallback(async (spaceCode) => {
|
|
69
|
+
const cacheKey = `${spaceCode}:${device}`;
|
|
70
|
+
if (cache.current[cacheKey])
|
|
71
|
+
return cache.current[cacheKey];
|
|
72
|
+
if (!visitorId)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const url = `${baseUrl}/sdk/serve/${encodeURIComponent(config.companyId)}?spaces=${encodeURIComponent(spaceCode)}&device=${device}&visitor_id=${encodeURIComponent(visitorId)}`;
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
headers: { 'api-key': config.apiKey },
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return null;
|
|
81
|
+
const json = await res.json();
|
|
82
|
+
const space = json.data.spaces.find((s) => s.space_code === spaceCode) || null;
|
|
83
|
+
if (space)
|
|
84
|
+
cache.current[cacheKey] = space;
|
|
85
|
+
return space;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}, [baseUrl, config.companyId, config.apiKey, device, visitorId]);
|
|
91
|
+
const flushImpressions = useCallback((screenName, retryCount = 0) => {
|
|
92
|
+
if (!visitorId || !isMounted.current)
|
|
93
|
+
return;
|
|
94
|
+
const ids = [...pendingImpressions.current];
|
|
95
|
+
if (!ids.length)
|
|
96
|
+
return;
|
|
97
|
+
pendingImpressions.current = [];
|
|
98
|
+
const pageUrl = encodeURIComponent(screenName || 'app://unknown');
|
|
99
|
+
const url = `${baseUrl}/sdk/impressions?company=${encodeURIComponent(config.companyId)}&placements=${ids.join(',')}&visitor_id=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}`;
|
|
100
|
+
fetch(url, { headers: { 'api-key': config.apiKey } }).catch(() => {
|
|
101
|
+
if (retryCount < MAX_RETRIES && isMounted.current) {
|
|
102
|
+
ids.forEach((id) => sentImpressions.current.delete(id));
|
|
103
|
+
pendingImpressions.current.push(...ids);
|
|
104
|
+
if (retryTimer.current)
|
|
105
|
+
clearTimeout(retryTimer.current);
|
|
106
|
+
retryTimer.current = setTimeout(() => flushImpressionsRef.current?.(screenName, retryCount + 1), RETRY_DELAY);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}, [baseUrl, config.companyId, config.apiKey, device, visitorId]);
|
|
110
|
+
useEffect(() => { flushImpressionsRef.current = flushImpressions; }, [flushImpressions]);
|
|
111
|
+
const trackImpression = useCallback((placementId, screenName) => {
|
|
112
|
+
if (sentImpressions.current.has(placementId))
|
|
113
|
+
return;
|
|
114
|
+
sentImpressions.current.add(placementId);
|
|
115
|
+
pendingImpressions.current.push(placementId);
|
|
116
|
+
if (debounceTimer.current)
|
|
117
|
+
clearTimeout(debounceTimer.current);
|
|
118
|
+
debounceTimer.current = setTimeout(() => flushImpressions(screenName), 1000);
|
|
119
|
+
}, [flushImpressions]);
|
|
120
|
+
const getClickUrl = useCallback((placementId, screenName) => {
|
|
121
|
+
const pageUrl = encodeURIComponent(screenName || 'app://unknown');
|
|
122
|
+
return `${baseUrl}/sdk/click/${encodeURIComponent(placementId)}?vid=${encodeURIComponent(visitorId)}&device=${device}&page_url=${pageUrl}`;
|
|
123
|
+
}, [baseUrl, visitorId, device]);
|
|
124
|
+
// Render children always — ad components handle the "not ready" state
|
|
125
|
+
return (_jsx(Ctx.Provider, { value: { config, visitorId, device, fetchSpace, trackImpression, getClickUrl }, children: children }));
|
|
126
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface MediaSuiteConfig {
|
|
2
|
+
/** MongoDB ObjectId of the company */
|
|
3
|
+
companyId: string;
|
|
4
|
+
/** Public embed API key (must be a restricted key with read-only SDK permissions) */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** API base URL. Defaults to https://api.mediasuite.com */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Ad {
|
|
10
|
+
placement_id: string;
|
|
11
|
+
image_url: string;
|
|
12
|
+
alt_text?: string;
|
|
13
|
+
click_url?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AdSpace {
|
|
16
|
+
space_code: string;
|
|
17
|
+
rotation_seconds: number;
|
|
18
|
+
ads: Ad[];
|
|
19
|
+
}
|
|
20
|
+
export interface ServeResponse {
|
|
21
|
+
success: boolean;
|
|
22
|
+
data: {
|
|
23
|
+
spaces: AdSpace[];
|
|
24
|
+
};
|
|
25
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@media-suite/reactnative-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MediaSuite Native SDK — React Native SDK for rendering and tracking ads in mobile apps. IAB/MRC viewability standards, impression/click tracking, and ad rotation.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepare": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@react-native-async-storage/async-storage": ">=1.19.0",
|
|
14
|
+
"react": ">=18.0.0",
|
|
15
|
+
"react-native": ">=0.72.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mediasuite",
|
|
19
|
+
"ads",
|
|
20
|
+
"react-native",
|
|
21
|
+
"sdk",
|
|
22
|
+
"advertising",
|
|
23
|
+
"viewability",
|
|
24
|
+
"IAB"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^19.2.14",
|
|
32
|
+
"@types/react-native": "^0.72.8",
|
|
33
|
+
"typescript": "^5.9.3"
|
|
34
|
+
}
|
|
35
|
+
}
|