diff --git a/examples/consentmanager/CHANGELOG.md b/examples/consentmanager/CHANGELOG.md index 9e89500..e0cf832 100644 --- a/examples/consentmanager/CHANGELOG.md +++ b/examples/consentmanager/CHANGELOG.md @@ -1,5 +1,12 @@ # @contentpass/examples-consentmanager +## 0.0.10 + +### Patch Changes + +- Updated dependencies + - @contentpass/react-native-contentpass-ui@0.8.1 + ## 0.0.9 ### Patch Changes diff --git a/examples/consentmanager/package.json b/examples/consentmanager/package.json index fa8eeaa..8b09ded 100644 --- a/examples/consentmanager/package.json +++ b/examples/consentmanager/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/examples-consentmanager", - "version": "0.0.9", + "version": "0.0.10", "main": "index.ts", "scripts": { "start": "expo start", diff --git a/packages/react-native-contentpass-ui/CHANGELOG.md b/packages/react-native-contentpass-ui/CHANGELOG.md index 885df2e..cdaa1ec 100644 --- a/packages/react-native-contentpass-ui/CHANGELOG.md +++ b/packages/react-native-contentpass-ui/CHANGELOG.md @@ -1,5 +1,11 @@ # @contentpass/react-native-contentpass-ui +## 0.8.1 + +### Patch Changes + +- Keep the first-layer footer above the Android system navigation bar so the privacy-policy link stays tappable. + ## 0.8.0 ### Minor Changes diff --git a/packages/react-native-contentpass-ui/package.json b/packages/react-native-contentpass-ui/package.json index 49566d8..0446d7d 100644 --- a/packages/react-native-contentpass-ui/package.json +++ b/packages/react-native-contentpass-ui/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/react-native-contentpass-ui", - "version": "0.8.0", + "version": "0.8.1", "description": "Contentpass React Native UI Components", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", diff --git a/packages/react-native-contentpass-ui/src/components/ContentpassLayer.tsx b/packages/react-native-contentpass-ui/src/components/ContentpassLayer.tsx index 774f75f..65fd1f1 100644 --- a/packages/react-native-contentpass-ui/src/components/ContentpassLayer.tsx +++ b/packages/react-native-contentpass-ui/src/components/ContentpassLayer.tsx @@ -1,15 +1,20 @@ import { ActivityIndicator, AppState, + Dimensions, Modal, + Platform, Pressable, + StatusBar, StyleSheet, Text, + useWindowDimensions, View, } from 'react-native'; import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import type { ContentpassLayerEvents } from './ContentpassLayerEvents'; import buildFirstLayerUrl from './buildFirstLayerUrl'; +import { getAndroidOverlayNavigationBarInset } from './ContentpassLayerAndroidInset'; import { canReachLayerUrl, getLayerLoadErrorCopy, @@ -38,6 +43,20 @@ type LayerReadyAction = export const LOAD_END_READY_FALLBACK_MS = 500; +function useAndroidOverlayNavigationBarInset(): number { + const window = useWindowDimensions(); + + if (Platform.OS !== 'android') { + return 0; + } + + return getAndroidOverlayNavigationBarInset({ + windowHeight: window.height, + screenHeight: Dimensions.get('screen').height, + statusBarHeight: StatusBar.currentHeight ?? 0, + }); +} + export function layerReadyReducer( ready: boolean, action: LayerReadyAction @@ -257,6 +276,8 @@ export default function ContentpassLayer({ vendorCount: number; locale?: string; }) { + const androidOverlayNavigationBarInset = + useAndroidOverlayNavigationBarInset(); const cacheNonce = useState(() => String(++firstLayerMountNonce))[0]; const firstLayerUrl = useMemo(() => { return buildFirstLayerUrl({ @@ -499,7 +520,14 @@ export default function ContentpassLayer({ } return ( - + 0 && { + paddingBottom: androidOverlayNavigationBarInset, + }, + ]} + > { + it('pads when the window is edge-to-edge', () => { + expect( + getAndroidOverlayNavigationBarInset({ + windowHeight: 800, + screenHeight: 800, + statusBarHeight: 24, + }) + ).toBe(ANDROID_NAVIGATION_BAR_HEIGHT_DP); + }); + + it('does not pad when the window already excludes the navigation bar', () => { + expect( + getAndroidOverlayNavigationBarInset({ + windowHeight: 728, + screenHeight: 800, + statusBarHeight: 24, + }) + ).toBe(0); + }); + + it('does not pad when only the navigation bar is reserved', () => { + expect( + getAndroidOverlayNavigationBarInset({ + windowHeight: 752, + screenHeight: 800, + statusBarHeight: 24, + }) + ).toBe(0); + }); + + it('pads when the status bar is reserved but the navigation bar overlays', () => { + expect( + getAndroidOverlayNavigationBarInset({ + windowHeight: 776, + screenHeight: 800, + statusBarHeight: 24, + }) + ).toBe(ANDROID_NAVIGATION_BAR_HEIGHT_DP); + }); +}); diff --git a/packages/react-native-contentpass-ui/src/components/ContentpassLayerAndroidInset.ts b/packages/react-native-contentpass-ui/src/components/ContentpassLayerAndroidInset.ts new file mode 100644 index 0000000..6127d7b --- /dev/null +++ b/packages/react-native-contentpass-ui/src/components/ContentpassLayerAndroidInset.ts @@ -0,0 +1,23 @@ +// Copyright 2026 Content Pass GmbH. All Rights Reserved. + +export const ANDROID_NAVIGATION_BAR_HEIGHT_DP = 48; + +export function getAndroidOverlayNavigationBarInset({ + windowHeight, + screenHeight, + statusBarHeight, +}: { + windowHeight: number; + screenHeight: number; + statusBarHeight: number; +}): number { + const reservedHeight = screenHeight - windowHeight; + + // The RN window is already shorter than the screen, so the WebView does + // not sit under the navigation bar. + if (reservedHeight > statusBarHeight + 1) { + return 0; + } + + return ANDROID_NAVIGATION_BAR_HEIGHT_DP; +}