@adtogether/react-native-sdk 0.1.7

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.
Files changed (41) hide show
  1. package/README.md +145 -0
  2. package/lib/commonjs/AdTogether.js +85 -0
  3. package/lib/commonjs/AdTogether.js.map +1 -0
  4. package/lib/commonjs/AdTogetherBanner.js +174 -0
  5. package/lib/commonjs/AdTogetherBanner.js.map +1 -0
  6. package/lib/commonjs/AdTogetherInterstitial.js +259 -0
  7. package/lib/commonjs/AdTogetherInterstitial.js.map +1 -0
  8. package/lib/commonjs/index.js +50 -0
  9. package/lib/commonjs/index.js.map +1 -0
  10. package/lib/commonjs/package.json +1 -0
  11. package/lib/commonjs/types.js +2 -0
  12. package/lib/commonjs/types.js.map +1 -0
  13. package/lib/module/AdTogether.js +80 -0
  14. package/lib/module/AdTogether.js.map +1 -0
  15. package/lib/module/AdTogetherBanner.js +168 -0
  16. package/lib/module/AdTogetherBanner.js.map +1 -0
  17. package/lib/module/AdTogetherInterstitial.js +253 -0
  18. package/lib/module/AdTogetherInterstitial.js.map +1 -0
  19. package/lib/module/index.js +7 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/types.js +2 -0
  23. package/lib/module/types.js.map +1 -0
  24. package/lib/typescript/AdTogether.d.ts +17 -0
  25. package/lib/typescript/AdTogether.d.ts.map +1 -0
  26. package/lib/typescript/AdTogetherBanner.d.ts +11 -0
  27. package/lib/typescript/AdTogetherBanner.d.ts.map +1 -0
  28. package/lib/typescript/AdTogetherInterstitial.d.ts +12 -0
  29. package/lib/typescript/AdTogetherInterstitial.d.ts.map +1 -0
  30. package/lib/typescript/index.d.ts +5 -0
  31. package/lib/typescript/index.d.ts.map +1 -0
  32. package/lib/typescript/types.d.ts +18 -0
  33. package/lib/typescript/types.d.ts.map +1 -0
  34. package/package.json +67 -0
  35. package/src/AdTogether.ts +98 -0
  36. package/src/AdTogetherBanner.tsx +175 -0
  37. package/src/AdTogetherInterstitial.tsx +230 -0
  38. package/src/index.ts +4 -0
  39. package/src/types.ts +19 -0
  40. package/tsconfig.build.json +10 -0
  41. package/tsconfig.json +23 -0
@@ -0,0 +1,17 @@
1
+ import { AdModel, AdType, AdTogetherOptions } from './types';
2
+ export declare class AdTogether {
3
+ private static instance;
4
+ private appId?;
5
+ private allowSelfAds;
6
+ baseUrl: string;
7
+ private constructor();
8
+ static get shared(): AdTogether;
9
+ static initialize(options: AdTogetherOptions): void;
10
+ assertInitialized(): boolean;
11
+ private lastAdId?;
12
+ static fetchAd(adUnitId: string, adType?: AdType): Promise<AdModel>;
13
+ static trackImpression(adId: string, token?: string): void;
14
+ static trackClick(adId: string, token?: string): void;
15
+ private static trackEvent;
16
+ }
17
+ //# sourceMappingURL=AdTogether.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdTogether.d.ts","sourceRoot":"","sources":["../../src/AdTogether.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAG7D,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAa;IACpC,OAAO,CAAC,KAAK,CAAC,CAAS;IACvB,OAAO,CAAC,YAAY,CAAiB;IAC9B,OAAO,EAAE,MAAM,CAA8C;IAEpE,OAAO;IAEP,MAAM,KAAK,MAAM,IAAI,UAAU,CAK9B;IAED,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,iBAAiB;IAe5C,iBAAiB;IAQjB,OAAO,CAAC,QAAQ,CAAC,CAAS;WAEb,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BzE,MAAM,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;IAInD,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;IAI9C,OAAO,CAAC,MAAM,CAAC,UAAU;CAe1B"}
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export interface AdTogetherBannerProps {
3
+ adUnitId: string;
4
+ onAdLoaded?: () => void;
5
+ onAdFailedToLoad?: (error: Error) => void;
6
+ /** Pass 'dark' to use dark mode, 'light' for light mode, or 'auto' (default) to respect system preference */
7
+ theme?: 'dark' | 'light' | 'auto';
8
+ style?: any;
9
+ }
10
+ export declare const AdTogetherBanner: React.FC<AdTogetherBannerProps>;
11
+ //# sourceMappingURL=AdTogetherBanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdTogetherBanner.d.ts","sourceRoot":"","sources":["../../src/AdTogetherBanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAK3D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C,6GAA6G;IAC7G,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IAClC,KAAK,CAAC,EAAE,GAAG,CAAC;CACb;AAED,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAwG5D,CAAC"}
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ export interface AdTogetherInterstitialProps {
3
+ adUnitId: string;
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ closeDelay?: number;
7
+ onAdLoaded?: () => void;
8
+ onAdFailedToLoad?: (error: Error) => void;
9
+ theme?: 'dark' | 'light' | 'auto';
10
+ }
11
+ export declare const AdTogetherInterstitial: React.FC<AdTogetherInterstitialProps>;
12
+ //# sourceMappingURL=AdTogetherInterstitial.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdTogetherInterstitial.d.ts","sourceRoot":"","sources":["../../src/AdTogetherInterstitial.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAK3D,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,KAAK,CAAC,EAAE,CAAC,2BAA2B,CA8HxE,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './AdTogether';
3
+ export * from './AdTogetherBanner';
4
+ export * from './AdTogetherInterstitial';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC"}
@@ -0,0 +1,18 @@
1
+ export type AdType = 'banner' | 'interstitial';
2
+ export interface AdModel {
3
+ id: string;
4
+ title: string;
5
+ description: string;
6
+ clickUrl?: string;
7
+ imageUrl?: string;
8
+ token?: string;
9
+ adType?: AdType;
10
+ }
11
+ export interface AdTogetherOptions {
12
+ appId?: string;
13
+ apiKey?: string;
14
+ baseUrl?: string;
15
+ /** Whether to show your own ads as fallback if no external ads are available. Defaults to true. */
16
+ allowSelfAds?: boolean;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,cAAc,CAAC;AAE/C,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@adtogether/react-native-sdk",
3
+ "version": "0.1.7",
4
+ "description": "The official AdTogether React Native SDK — reciprocal ad exchange to increase conversions and grow your audience.",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "react-native": "src/index.ts",
8
+ "source": "src/index.ts",
9
+ "types": "lib/typescript/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./lib/module/index.js",
13
+ "require": "./lib/commonjs/index.js",
14
+ "types": "./lib/typescript/index.d.ts"
15
+ }
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/undecided2003/AdTogether.git",
20
+ "directory": "sdk/react-native-sdk"
21
+ },
22
+ "homepage": "https://adtogether.relaxsoftwareapps.com",
23
+ "bugs": {
24
+ "url": "https://github.com/undecided2003/AdTogether/issues"
25
+ },
26
+ "scripts": {
27
+ "build": "bob build",
28
+ "dev": "bob build --watch",
29
+ "prepack": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "react-native",
33
+ "ads",
34
+ "ad-exchange",
35
+ "monetization",
36
+ "advertising",
37
+ "reciprocal",
38
+ "conversions"
39
+ ],
40
+ "author": "AdTogether Team",
41
+ "license": "MIT",
42
+ "peerDependencies": {
43
+ "react": "*",
44
+ "react-native": "*"
45
+ },
46
+ "devDependencies": {
47
+ "@types/react": "^18.2.0",
48
+ "react": "18.2.0",
49
+ "react-native": "0.74.0",
50
+ "react-native-builder-bob": "^0.30.2",
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "react-native-builder-bob": {
54
+ "source": "src",
55
+ "output": "lib",
56
+ "targets": [
57
+ "commonjs",
58
+ "module",
59
+ [
60
+ "typescript",
61
+ {
62
+ "project": "tsconfig.build.json"
63
+ }
64
+ ]
65
+ ]
66
+ }
67
+ }
@@ -0,0 +1,98 @@
1
+ import { AdModel, AdType, AdTogetherOptions } from './types';
2
+ import { Platform } from 'react-native';
3
+
4
+ export class AdTogether {
5
+ private static instance: AdTogether;
6
+ private appId?: string;
7
+ private allowSelfAds: boolean = true;
8
+ public baseUrl: string = 'https://adtogether.relaxsoftwareapps.com';
9
+
10
+ private constructor() {}
11
+
12
+ static get shared(): AdTogether {
13
+ if (!AdTogether.instance) {
14
+ AdTogether.instance = new AdTogether();
15
+ }
16
+ return AdTogether.instance;
17
+ }
18
+
19
+ static initialize(options: AdTogetherOptions) {
20
+ const sdk = AdTogether.shared;
21
+ sdk.appId = options.apiKey || options.appId;
22
+
23
+ if (options.allowSelfAds !== undefined) {
24
+ sdk.allowSelfAds = options.allowSelfAds;
25
+ }
26
+
27
+ if (options.baseUrl) {
28
+ sdk.baseUrl = options.baseUrl;
29
+ }
30
+
31
+ console.log(`AdTogether SDK Initialized with App ID: ${sdk.appId}`);
32
+ }
33
+
34
+ assertInitialized() {
35
+ if (!this.appId) {
36
+ console.error('AdTogether Error: SDK has not been initialized. Please call AdTogether.initialize() before displaying ads.');
37
+ return false;
38
+ }
39
+ return true;
40
+ }
41
+
42
+ private lastAdId?: string;
43
+
44
+ static async fetchAd(adUnitId: string, adType?: AdType): Promise<AdModel> {
45
+ if (!AdTogether.shared.assertInitialized()) {
46
+ throw new Error('AdTogether not initialized');
47
+ }
48
+
49
+ try {
50
+ const sdk = AdTogether.shared;
51
+ let url = `${sdk.baseUrl}/api/ads/serve?country=global&adUnitId=${adUnitId}&apiKey=${sdk.appId}`;
52
+ if (adType) {
53
+ url += `&adType=${adType}`;
54
+ }
55
+ if (sdk.lastAdId) {
56
+ url += `&exclude=${sdk.lastAdId}`;
57
+ }
58
+
59
+ // Pass allowSelfAds
60
+ url += `&allowSelfAds=${sdk.allowSelfAds}&platform=${Platform.OS}`;
61
+
62
+ const response = await fetch(url);
63
+ if (response.ok) {
64
+ const ad = await response.json();
65
+ sdk.lastAdId = ad.id;
66
+ return ad;
67
+ }
68
+
69
+ throw new Error(`Failed to fetch ad. Status: ${response.status}`);
70
+ } catch (err) {
71
+ throw err;
72
+ }
73
+ }
74
+
75
+ static trackImpression(adId: string, token?: string) {
76
+ this.trackEvent('/api/ads/impression', adId, token);
77
+ }
78
+
79
+ static trackClick(adId: string, token?: string) {
80
+ this.trackEvent('/api/ads/click', adId, token);
81
+ }
82
+
83
+ private static trackEvent(endpoint: string, adId: string, token?: string) {
84
+ if (!AdTogether.shared.assertInitialized()) return;
85
+
86
+ fetch(`${AdTogether.shared.baseUrl}${endpoint}`, {
87
+ method: 'POST',
88
+ headers: {
89
+ 'Content-Type': 'application/json',
90
+ },
91
+ body: JSON.stringify({
92
+ adId,
93
+ token,
94
+ apiKey: AdTogether.shared.appId
95
+ }),
96
+ }).catch(console.error);
97
+ }
98
+ }
@@ -0,0 +1,175 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { View, Text, Image, TouchableOpacity, StyleSheet, useColorScheme, Linking } from 'react-native';
3
+ import { AdTogether } from './AdTogether';
4
+ import { AdModel } from './types';
5
+
6
+ export interface AdTogetherBannerProps {
7
+ adUnitId: string;
8
+ onAdLoaded?: () => void;
9
+ onAdFailedToLoad?: (error: Error) => void;
10
+ /** Pass 'dark' to use dark mode, 'light' for light mode, or 'auto' (default) to respect system preference */
11
+ theme?: 'dark' | 'light' | 'auto';
12
+ style?: any;
13
+ }
14
+
15
+ export const AdTogetherBanner: React.FC<AdTogetherBannerProps> = ({
16
+ adUnitId,
17
+ onAdLoaded,
18
+ onAdFailedToLoad,
19
+ theme = 'auto',
20
+ style,
21
+ }) => {
22
+ const [adData, setAdData] = useState<AdModel | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [hasError, setHasError] = useState(false);
25
+ const systemColorScheme = useColorScheme();
26
+
27
+ const impressionTrackedRef = useRef(false);
28
+
29
+ useEffect(() => {
30
+ let isMounted = true;
31
+
32
+ AdTogether.fetchAd(adUnitId, 'banner')
33
+ .then((ad) => {
34
+ if (isMounted) {
35
+ setAdData(ad);
36
+ setIsLoading(false);
37
+ onAdLoaded?.();
38
+ }
39
+ })
40
+ .catch((err) => {
41
+ if (isMounted) {
42
+ console.error('AdTogether Failed to load ad:', err);
43
+ setHasError(true);
44
+ setIsLoading(false);
45
+ onAdFailedToLoad?.(err);
46
+ }
47
+ });
48
+
49
+ return () => {
50
+ isMounted = false;
51
+ };
52
+ }, [adUnitId, onAdLoaded, onAdFailedToLoad]);
53
+
54
+ // Track impression when ad is rendered
55
+ useEffect(() => {
56
+ if (adData && !isLoading && !hasError && !impressionTrackedRef.current) {
57
+ impressionTrackedRef.current = true;
58
+ AdTogether.trackImpression(adData.id, adData.token);
59
+ }
60
+ }, [adData, isLoading, hasError]);
61
+
62
+ const handlePress = () => {
63
+ if (!adData) return;
64
+ AdTogether.trackClick(adData.id, adData.token);
65
+ if (adData.clickUrl) {
66
+ Linking.openURL(adData.clickUrl).catch(console.error);
67
+ }
68
+ };
69
+
70
+ if (isLoading) {
71
+ return <View style={[styles.container, style]} />;
72
+ }
73
+
74
+ if (hasError || !adData) {
75
+ return null;
76
+ }
77
+
78
+ const isDarkMode = theme === 'auto' ? systemColorScheme === 'dark' : theme === 'dark';
79
+
80
+ const bgColor = isDarkMode ? '#1F2937' : '#ffffff';
81
+ const borderColor = isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
82
+ const textColor = isDarkMode ? '#F9FAFB' : '#111827';
83
+ const descColor = isDarkMode ? '#9CA3AF' : '#6B7280';
84
+
85
+ return (
86
+ <TouchableOpacity
87
+ activeOpacity={0.8}
88
+ onPress={handlePress}
89
+ style={[
90
+ styles.container,
91
+ { backgroundColor: bgColor, borderColor },
92
+ style,
93
+ ]}
94
+ >
95
+ {adData.imageUrl ? (
96
+ <View style={styles.imageContainer}>
97
+ <Image
98
+ source={{ uri: adData.imageUrl }}
99
+ style={styles.image}
100
+ resizeMode="cover"
101
+ />
102
+ </View>
103
+ ) : null}
104
+ <View style={styles.textContainer}>
105
+ <View style={styles.headerRow}>
106
+ <Text style={[styles.title, { color: textColor }]} numberOfLines={1}>
107
+ {adData.title}
108
+ </Text>
109
+ <View style={styles.adBadge}>
110
+ <Text style={styles.adBadgeText}>AD</Text>
111
+ </View>
112
+ </View>
113
+ <Text style={[styles.description, { color: descColor }]} numberOfLines={2}>
114
+ {adData.description}
115
+ </Text>
116
+ </View>
117
+ </TouchableOpacity>
118
+ );
119
+ };
120
+
121
+ const styles = StyleSheet.create({
122
+ container: {
123
+ flexDirection: 'row',
124
+ width: '100%',
125
+ minHeight: 80,
126
+ borderRadius: 12,
127
+ borderWidth: 1,
128
+ overflow: 'hidden',
129
+ shadowColor: '#000',
130
+ shadowOffset: { width: 0, height: 2 },
131
+ shadowOpacity: 0.05,
132
+ shadowRadius: 5,
133
+ elevation: 2,
134
+ },
135
+ imageContainer: {
136
+ width: 120,
137
+ height: '100%',
138
+ minHeight: 80,
139
+ },
140
+ image: {
141
+ width: '100%',
142
+ height: '100%',
143
+ },
144
+ textContainer: {
145
+ padding: 12,
146
+ flex: 1,
147
+ justifyContent: 'center',
148
+ },
149
+ headerRow: {
150
+ flexDirection: 'row',
151
+ justifyContent: 'space-between',
152
+ alignItems: 'flex-start',
153
+ marginBottom: 4,
154
+ },
155
+ title: {
156
+ fontWeight: 'bold',
157
+ fontSize: 14,
158
+ flex: 1,
159
+ },
160
+ adBadge: {
161
+ backgroundColor: '#FBBF24',
162
+ paddingHorizontal: 4,
163
+ paddingVertical: 2,
164
+ borderRadius: 4,
165
+ marginLeft: 8,
166
+ },
167
+ adBadgeText: {
168
+ color: '#000',
169
+ fontSize: 9,
170
+ fontWeight: 'bold',
171
+ },
172
+ description: {
173
+ fontSize: 12,
174
+ },
175
+ });
@@ -0,0 +1,230 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { View, Text, Image, TouchableOpacity, StyleSheet, Modal, SafeAreaView, useColorScheme, Linking } from 'react-native';
3
+ import { AdTogether } from './AdTogether';
4
+ import { AdModel } from './types';
5
+
6
+ export interface AdTogetherInterstitialProps {
7
+ adUnitId: string;
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ closeDelay?: number;
11
+ onAdLoaded?: () => void;
12
+ onAdFailedToLoad?: (error: Error) => void;
13
+ theme?: 'dark' | 'light' | 'auto';
14
+ }
15
+
16
+ export const AdTogetherInterstitial: React.FC<AdTogetherInterstitialProps> = ({
17
+ adUnitId,
18
+ isOpen,
19
+ onClose,
20
+ closeDelay = 3,
21
+ onAdLoaded,
22
+ onAdFailedToLoad,
23
+ theme = 'auto',
24
+ }) => {
25
+ const [adData, setAdData] = useState<AdModel | null>(null);
26
+ const [isLoading, setIsLoading] = useState(true);
27
+ const [hasError, setHasError] = useState(false);
28
+ const [timeLeft, setTimeLeft] = useState(closeDelay);
29
+ const systemColorScheme = useColorScheme();
30
+
31
+ const impressionTrackedRef = useRef(false);
32
+
33
+ useEffect(() => {
34
+ if (!isOpen) return;
35
+
36
+ let isMounted = true;
37
+ setIsLoading(true);
38
+ setHasError(false);
39
+ setTimeLeft(closeDelay);
40
+ impressionTrackedRef.current = false;
41
+
42
+ AdTogether.fetchAd(adUnitId, 'interstitial')
43
+ .then((ad) => {
44
+ if (isMounted) {
45
+ setAdData(ad);
46
+ setIsLoading(false);
47
+ onAdLoaded?.();
48
+ }
49
+ })
50
+ .catch((err) => {
51
+ if (isMounted) {
52
+ console.error('AdTogether Interstitial Failed:', err);
53
+ setHasError(true);
54
+ setIsLoading(false);
55
+ onAdFailedToLoad?.(err);
56
+ onClose(); // Automatically close on error
57
+ }
58
+ });
59
+
60
+ return () => {
61
+ isMounted = false;
62
+ };
63
+ }, [adUnitId, isOpen, closeDelay, onAdLoaded, onAdFailedToLoad, onClose]);
64
+
65
+ // Track impression and start timer
66
+ useEffect(() => {
67
+ if (isOpen && adData && !isLoading && !hasError && !impressionTrackedRef.current) {
68
+ impressionTrackedRef.current = true;
69
+ AdTogether.trackImpression(adData.id, adData.token);
70
+
71
+ const timer = setInterval(() => {
72
+ setTimeLeft((prev) => {
73
+ if (prev <= 1) {
74
+ clearInterval(timer);
75
+ return 0;
76
+ }
77
+ return prev - 1;
78
+ });
79
+ }, 1000);
80
+
81
+ return () => clearInterval(timer);
82
+ }
83
+ }, [isOpen, adData, isLoading, hasError]);
84
+
85
+ const handlePress = () => {
86
+ if (!adData) return;
87
+ AdTogether.trackClick(adData.id, adData.token);
88
+ if (adData.clickUrl) {
89
+ Linking.openURL(adData.clickUrl).catch(console.error);
90
+ }
91
+ };
92
+
93
+ if (!isOpen || isLoading || hasError || !adData) {
94
+ return null;
95
+ }
96
+
97
+ const isDarkMode = theme === 'auto' ? systemColorScheme === 'dark' : theme === 'dark';
98
+ const bgColor = isDarkMode ? '#111827' : '#ffffff';
99
+ const textColor = isDarkMode ? '#F9FAFB' : '#111827';
100
+ const descColor = isDarkMode ? '#9CA3AF' : '#6B7280';
101
+ const buttonBgColor = isDarkMode ? '#374151' : '#F3F4F6';
102
+
103
+ return (
104
+ <Modal visible={isOpen} animationType="slide" transparent={false}>
105
+ <SafeAreaView style={[styles.container, { backgroundColor: bgColor }]}>
106
+ <View style={styles.header}>
107
+ {timeLeft > 0 ? (
108
+ <View style={[styles.closeButton, { backgroundColor: buttonBgColor }]}>
109
+ <Text style={{ color: descColor, fontWeight: 'bold' }}>{timeLeft}</Text>
110
+ </View>
111
+ ) : (
112
+ <TouchableOpacity onPress={onClose} style={[styles.closeButton, { backgroundColor: buttonBgColor }]}>
113
+ <Text style={{ color: textColor, fontWeight: 'bold', fontSize: 16 }}>✕</Text>
114
+ </TouchableOpacity>
115
+ )}
116
+ </View>
117
+
118
+ <TouchableOpacity activeOpacity={0.9} style={styles.content} onPress={handlePress}>
119
+ <View style={styles.imageWrapper}>
120
+ {adData.imageUrl ? (
121
+ <Image source={{ uri: adData.imageUrl }} style={styles.image} resizeMode="contain" />
122
+ ) : (
123
+ <View style={[styles.imagePlaceholder, { backgroundColor: isDarkMode ? '#1F2937' : '#E5E7EB' }]} />
124
+ )}
125
+ </View>
126
+
127
+ <View style={styles.textWrapper}>
128
+ <View style={styles.adBadge}>
129
+ <Text style={styles.adBadgeText}>Advertisement</Text>
130
+ </View>
131
+ <Text style={[styles.title, { color: textColor }]}>{adData.title}</Text>
132
+ <Text style={[styles.description, { color: descColor }]}>{adData.description}</Text>
133
+
134
+ <View style={styles.ctaButton}>
135
+ <Text style={styles.ctaText}>Learn More</Text>
136
+ </View>
137
+ </View>
138
+ </TouchableOpacity>
139
+ </SafeAreaView>
140
+ </Modal>
141
+ );
142
+ };
143
+
144
+ const styles = StyleSheet.create({
145
+ container: {
146
+ flex: 1,
147
+ },
148
+ header: {
149
+ flexDirection: 'row',
150
+ justifyContent: 'flex-end',
151
+ padding: 16,
152
+ zIndex: 10,
153
+ },
154
+ closeButton: {
155
+ width: 32,
156
+ height: 32,
157
+ borderRadius: 16,
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ },
161
+ content: {
162
+ flex: 1,
163
+ padding: 24,
164
+ paddingTop: 0,
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ },
168
+ imageWrapper: {
169
+ width: '100%',
170
+ aspectRatio: 1,
171
+ marginBottom: 32,
172
+ shadowColor: '#000',
173
+ shadowOffset: { width: 0, height: 10 },
174
+ shadowOpacity: 0.1,
175
+ shadowRadius: 20,
176
+ elevation: 5,
177
+ },
178
+ image: {
179
+ width: '100%',
180
+ height: '100%',
181
+ borderRadius: 16,
182
+ },
183
+ imagePlaceholder: {
184
+ width: '100%',
185
+ height: '100%',
186
+ borderRadius: 16,
187
+ },
188
+ textWrapper: {
189
+ alignItems: 'center',
190
+ width: '100%',
191
+ },
192
+ adBadge: {
193
+ backgroundColor: '#FBBF24',
194
+ paddingHorizontal: 8,
195
+ paddingVertical: 4,
196
+ borderRadius: 6,
197
+ marginBottom: 16,
198
+ },
199
+ adBadgeText: {
200
+ color: '#000',
201
+ fontSize: 10,
202
+ fontWeight: '900',
203
+ textTransform: 'uppercase',
204
+ },
205
+ title: {
206
+ fontSize: 24,
207
+ fontWeight: 'bold',
208
+ textAlign: 'center',
209
+ marginBottom: 12,
210
+ },
211
+ description: {
212
+ fontSize: 16,
213
+ textAlign: 'center',
214
+ lineHeight: 24,
215
+ marginBottom: 32,
216
+ },
217
+ ctaButton: {
218
+ backgroundColor: '#3B82F6',
219
+ paddingHorizontal: 32,
220
+ paddingVertical: 16,
221
+ borderRadius: 12,
222
+ width: '100%',
223
+ alignItems: 'center',
224
+ },
225
+ ctaText: {
226
+ color: '#FFFFFF',
227
+ fontSize: 16,
228
+ fontWeight: 'bold',
229
+ },
230
+ });
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './AdTogether';
3
+ export * from './AdTogetherBanner';
4
+ export * from './AdTogetherInterstitial';
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ export type AdType = 'banner' | 'interstitial';
2
+
3
+ export interface AdModel {
4
+ id: string;
5
+ title: string;
6
+ description: string;
7
+ clickUrl?: string;
8
+ imageUrl?: string;
9
+ token?: string;
10
+ adType?: AdType;
11
+ }
12
+
13
+ export interface AdTogetherOptions {
14
+ appId?: string;
15
+ apiKey?: string;
16
+ baseUrl?: string;
17
+ /** Whether to show your own ads as fallback if no external ads are available. Defaults to true. */
18
+ allowSelfAds?: boolean;
19
+ }