Skip to content

Commit 81a5d1c

Browse files
committed
feat: handle first layer footer links
This commit introduces handling of footer links clicked in the first layer. They will be opened in a separate webview in a popup.
1 parent d644b6f commit 81a5d1c

3 files changed

Lines changed: 103 additions & 9 deletions

File tree

packages/react-native-contentpass-ui/src/components/ContentpassConsentGate.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export default function ContentpassConsentGate({
165165
<ContentpassLayer
166166
eventHandler={layerEvents}
167167
baseUrl={contentpassConfig.apiUrl}
168+
instanceId={sdk.instanceId}
168169
planId={contentpassConfig.planId}
169170
propertyId={contentpassConfig.propertyId}
170171
purposesList={purposesList}

packages/react-native-contentpass-ui/src/components/ContentpassLayer.tsx

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
1+
import {
2+
ActivityIndicator,
3+
Modal,
4+
Pressable,
5+
StyleSheet,
6+
Text,
7+
View,
8+
} from 'react-native';
29
import { WebView, type WebViewMessageEvent } from 'react-native-webview';
310
import type { ContentpassLayerEvents } from './ContentpassLayerEvents';
411
import buildFirstLayerUrl from './buildFirstLayerUrl';
5-
import { useMemo, useState } from 'react';
12+
import { useCallback, useMemo, useState } from 'react';
613

714
const MESSAGE_PROTOCOL = 'contentpass-first-layer';
815

@@ -52,18 +59,44 @@ const styles = StyleSheet.create({
5259
alignItems: 'center',
5360
justifyContent: 'center',
5461
},
62+
popupContainer: {
63+
flex: 1,
64+
backgroundColor: '#fff',
65+
},
66+
popupHeader: {
67+
flexDirection: 'row',
68+
justifyContent: 'flex-end',
69+
paddingHorizontal: 12,
70+
paddingVertical: 8,
71+
borderBottomWidth: StyleSheet.hairlineWidth,
72+
borderBottomColor: '#ccc',
73+
},
74+
popupClose: {
75+
paddingHorizontal: 12,
76+
paddingVertical: 6,
77+
},
78+
popupCloseText: {
79+
fontSize: 16,
80+
fontWeight: '600',
81+
color: '#007AFF',
82+
},
83+
popupWebview: {
84+
flex: 1,
85+
},
5586
});
5687

5788
export default function ContentpassLayer({
5889
baseUrl,
5990
eventHandler,
91+
instanceId,
6092
planId,
6193
propertyId,
6294
purposesList,
6395
vendorCount,
6496
}: {
6597
baseUrl: string;
6698
eventHandler: ContentpassLayerEvents;
99+
instanceId: string;
67100
planId: string;
68101
propertyId: string;
69102
purposesList: string[];
@@ -80,6 +113,13 @@ export default function ContentpassLayer({
80113
}, [baseUrl, planId, propertyId, purposesList, vendorCount]);
81114

82115
const [ready, setReady] = useState(false);
116+
const [popupUrl, setPopupUrl] = useState<string | null>(null);
117+
118+
const closePopup = useCallback(() => setPopupUrl(null), []);
119+
120+
function buildFaqUrl(): string {
121+
return `${baseUrl}/auth/login?instanceId=${encodeURIComponent(instanceId)}&propertyId=${encodeURIComponent(propertyId)}&planId=${encodeURIComponent(planId)}&route=faq`;
122+
}
83123

84124
function handleMessage(event: WebViewMessageEvent) {
85125
let msg: any;
@@ -118,6 +158,19 @@ export default function ContentpassLayer({
118158
msg.payload?.options?.page as 'login' | 'signup'
119159
);
120160
break;
161+
case 'faq':
162+
setPopupUrl(buildFaqUrl());
163+
break;
164+
case 'url':
165+
if (msg.payload?.options?.url) {
166+
setPopupUrl(msg.payload?.options?.url);
167+
} else {
168+
console.warn(
169+
'WebView message with unknown URL',
170+
msg.payload?.options?.url
171+
);
172+
}
173+
break;
121174
default:
122175
console.warn(
123176
'WebView message with unknown page',
@@ -167,8 +220,16 @@ export default function ContentpassLayer({
167220
handleMessage(event);
168221
}}
169222
onShouldStartLoadWithRequest={(request) => {
170-
console.debug('WebView request', request.url);
171-
return true;
223+
// Prevent accidental redirects to external URLs
224+
const firstLayerHostname = new URL(firstLayerUrl).hostname;
225+
const requestedHostname = new URL(request.url).hostname;
226+
const allowed = requestedHostname === firstLayerHostname;
227+
console.debug('WebView request', request.url, {
228+
allowed,
229+
firstLayerHostname,
230+
requestedHostname,
231+
});
232+
return allowed;
172233
}}
173234
onLoadStart={() => {
174235
console.debug('WebView load start');
@@ -198,6 +259,34 @@ export default function ContentpassLayer({
198259
<ActivityIndicator size="large" />
199260
</View>
200261
)}
262+
<Modal
263+
visible={popupUrl !== null}
264+
animationType="slide"
265+
presentationStyle="pageSheet"
266+
onRequestClose={closePopup}
267+
>
268+
<View style={styles.popupContainer}>
269+
<View style={styles.popupHeader}>
270+
<Pressable onPress={closePopup} style={styles.popupClose}>
271+
<Text style={styles.popupCloseText}>Close</Text>
272+
</Pressable>
273+
</View>
274+
{popupUrl && (
275+
<WebView
276+
source={{ uri: popupUrl }}
277+
style={styles.popupWebview}
278+
javaScriptEnabled
279+
domStorageEnabled
280+
onShouldStartLoadWithRequest={(request) => {
281+
console.debug('WebView popup request', request.url);
282+
// Allow any request to load in the popup, otherwise
283+
// we would block redirects to external URLs.
284+
return true;
285+
}}
286+
/>
287+
)}
288+
</View>
289+
</Modal>
201290
</View>
202291
);
203292
}

packages/react-native-contentpass/src/Contentpass.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export default class Contentpass implements ContentpassInterface {
4444
private authStateStorage: OidcAuthStateStorage;
4545
private readonly config: ContentpassConfig;
4646
private readonly samplingRate: number;
47-
private instanceId: string;
47+
private _instanceId: string;
48+
49+
get instanceId(): string {
50+
return this._instanceId;
51+
}
4852

4953
private contentpassState: ContentpassState = {
5054
state: ContentpassStateType.INITIALISING,
@@ -59,7 +63,7 @@ export default class Contentpass implements ContentpassInterface {
5963
}
6064

6165
this.config = config;
62-
this.instanceId = uuid.v4();
66+
this._instanceId = uuid.v4();
6367
logger.debug('Contentpass initialised with config', config);
6468
if (
6569
config.samplingRate &&
@@ -130,7 +134,7 @@ export default class Contentpass implements ContentpassInterface {
130134
ea: eventAction,
131135
ec: eventCategory,
132136
el: eventLabel,
133-
cpabid: this.instanceId,
137+
cpabid: this._instanceId,
134138
cppid: publicId,
135139
cpsr: activeSamplingRate,
136140
};
@@ -180,7 +184,7 @@ export default class Contentpass implements ContentpassInterface {
180184
public countImpression = async () => {
181185
// Generate a new impression id to be used per impression
182186
// This mimics the behaviour of the web SDK when in SPA mode
183-
this.instanceId = uuid.v4();
187+
this._instanceId = uuid.v4();
184188
await Promise.all([
185189
this.countPaidImpressionWhenUserHasValidSub(),
186190
this.countSampledImpression(),
@@ -197,7 +201,7 @@ export default class Contentpass implements ContentpassInterface {
197201
try {
198202
await sendPageViewEvent(this.config.apiUrl, {
199203
propertyId: this.config.propertyId,
200-
impressionId: this.instanceId,
204+
impressionId: this._instanceId,
201205
accessToken: this.oidcAuthState!.accessToken,
202206
});
203207
} catch (err: any) {

0 commit comments

Comments
 (0)