From 135976172ee3037d5c18f799acd2632f99f20065 Mon Sep 17 00:00:00 2001 From: Guilherme Albert Date: Mon, 5 Oct 2026 21:00:19 -0300 Subject: [PATCH] feat: open legal documents in managed webview --- .knowledge/README.md | 1 + .knowledge/integrations/embedded_content.md | 30 +++ .knowledge/integrations/shared_packages.md | 12 +- ios/Podfile.lock | 26 ++ package-lock.json | 282 +++++++++++--------- package.json | 10 +- src/app/routes/web-content.tsx | 1 + src/app/web-content/index.tsx | 143 ++++++++++ src/app/web-content/legal-content.ts | 25 ++ src/components/app-drawer/index.tsx | 34 +++ src/i18n/messages.ts | 15 ++ src/i18n/types.ts | 9 + tests/app/legal-content.test.ts | 22 ++ tests/app/web-content-screen.test.tsx | 99 +++++++ tests/components/app-header.test.tsx | 17 +- tests/contracts/app-config.test.ts | 8 +- tests/contracts/shared-packages.test.ts | 9 + tests/i18n/messages.test.ts | 19 ++ 18 files changed, 625 insertions(+), 137 deletions(-) create mode 100644 .knowledge/integrations/embedded_content.md create mode 100644 src/app/routes/web-content.tsx create mode 100644 src/app/web-content/index.tsx create mode 100644 src/app/web-content/legal-content.ts create mode 100644 tests/app/legal-content.test.ts create mode 100644 tests/app/web-content-screen.test.tsx diff --git a/.knowledge/README.md b/.knowledge/README.md index 33f5e7b..d6d6978 100644 --- a/.knowledge/README.md +++ b/.knowledge/README.md @@ -25,6 +25,7 @@ Other web areas are intentionally outside the mobile scope. - [Design system](design_system/README.md) — mobile visual authority and documentation index - [Foundations and components](design_system/foundations_and_components.md) - [Shared packages](integrations/shared_packages.md) — ownership and local consumption of public NPM packages +- [Embedded content](integrations/embedded_content.md) — trusted legal-document WebView and recovery policy - [Android versioning](integrations/android_versioning.md) — Firebase minimum version and Google Play update behavior ## Authority diff --git a/.knowledge/integrations/embedded_content.md b/.knowledge/integrations/embedded_content.md new file mode 100644 index 0000000..07a452e --- /dev/null +++ b/.knowledge/integrations/embedded_content.md @@ -0,0 +1,30 @@ +# Embedded Content + +> Define trusted in-app legal documents, navigation boundaries, and recovery behavior. + +Openings embeds only the public privacy policy and terms of service from +`https://openings.dev`. The drawer sends a closed `privacy` or `terms` identifier +to the native web-content route. The screen maps that identifier to a compile-time +URL; route parameters never supply a URL directly. + +## Navigation policy + +The public `@trebla/managed-webview` package owns WebView lifecycle and navigation +policy. Privacy allows only the exact `/privacy` path and Terms allows only the +exact `/terms` path on the `https://openings.dev` origin. HTTP, credentials, +malformed URLs, other origins, and other paths never load inside the WebView. + +The app uses main-document load readiness, a ten-second timeout, and the package's +built-in challenge detection. Loading content stays behind an opaque native cover. +A timeout, network or HTTP failure, process termination, detected challenge, or +external navigation unmounts the document and shows native recovery. + +## Recovery and privacy + +Recovery offers retry and an explicit browser action when the package provides a +validated fallback target. The app never opens another application automatically. +It sends no search text, saved jobs, viewed state, preferences, notification state, +analytics state, or other local data in legal-document URLs. + +Openings owns the native header, six-locale labels, theme values, and recovery +presentation. The website remains the source of truth for the legal prose. diff --git a/.knowledge/integrations/shared_packages.md b/.knowledge/integrations/shared_packages.md index 11f2fc7..b255d3a 100644 --- a/.knowledge/integrations/shared_packages.md +++ b/.knowledge/integrations/shared_packages.md @@ -1,6 +1,6 @@ # Shared Packages -> Define ownership and local consumption of the two public Openings foundation packages. +> Define ownership and local consumption of public packages used by Openings Mobile. ## Packages @@ -17,6 +17,12 @@ Both packages are public, MIT-licensed, independently versioned NPM artifacts. T mobile application is private and must never be published as part of their package contents. +`@trebla/managed-webview` is a public React Native infrastructure package. It owns +HTTPS origin and path policy, readiness, timeout, challenge detection, failure +unmounting, retry, and explicit browser fallback. Openings owns its document URL +catalog, navigation route, localization, theme, and recovery presentation. See +[Embedded content](embedded_content.md) for the consumer contract. + ## Local development Mobile resolves exact public npm releases and Metro uses the standard project @@ -24,7 +30,7 @@ configuration. A fresh clone needs no sibling repository. Changes to either shar package must be published under semantic versioning before mobile deliberately updates its exact dependency. -Jest transforms the package boundary explicitly, and the contract test imports the +Jest transforms package boundaries explicitly, and contract tests import the same public exports the application consumes. Do not bypass package exports with a relative import into another repository's `src` folder. @@ -35,6 +41,8 @@ relative import into another repository's `src` folder. system, not a screen-specific layout choice. - Keep React Native components, Expo APIs, device services, navigation, and native persistence inside mobile. +- Keep Openings-specific URLs, copy, and visual presentation out of + `@trebla/managed-webview`. - Keep Next.js, DOM, and browser-only behavior inside web. - Preserve semantic versioning and add package tests before changing an exported contract. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index af2f1b6..2a9c0c1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1742,6 +1742,28 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga + - react-native-webview (13.16.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga - React-NativeModulesApple (0.86.3): - hermes-engine - React-callinvoker @@ -2538,6 +2560,7 @@ DEPENDENCIES: - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - React-mutationobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) @@ -2718,6 +2741,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-networking: @@ -2873,6 +2898,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: 97a771dbbb4315c71579995b53e4e3e351a99d89 React-mutationobservernativemodule: 530bff1bba9511a5133e3cf8170e3b2c0df49e89 react-native-safe-area-context: 6b4966397ada0f7dd481e4486a2ef936834861a1 + react-native-webview: 2a4da2e03d3ec2c731c4d1a389cb86aaa4902167 React-NativeModulesApple: 2b658272ca69df0694cf5d54f9510aa7c33977bc React-networking: 6be0b80e18e710dd30e77b04838ec46666a7bb78 React-oscompat: cea7c66df8076bd97e95885d7f584b86afffde41 diff --git a/package-lock.json b/package-lock.json index fb815a1..a21f51c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,15 +19,16 @@ "@react-native-firebase/remote-config": "26.3.3", "@sentry/react-native": "~7.11.0", "@tanstack/react-query": "^5.102.8", - "expo": "~57.0.24", + "@trebla/managed-webview": "0.2.0", + "expo": "~57.0.26", "expo-application": "~57.0.3", - "expo-constants": "~57.0.19", + "expo-constants": "~57.0.20", "expo-dev-client": "~57.0.19", "expo-font": "~57.0.4", "expo-in-app-updates": "0.12.0", - "expo-linking": "~57.0.10", + "expo-linking": "~57.0.11", "expo-localization": "~57.0.2", - "expo-router": "~57.0.22", + "expo-router": "~57.0.24", "expo-splash-screen": "~57.0.9", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.4", @@ -44,6 +45,7 @@ "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.26.0", "react-native-svg": "15.15.4", + "react-native-webview": "13.16.1", "react-native-worklets": "0.10.1" }, "devDependencies": { @@ -1694,9 +1696,9 @@ } }, "node_modules/@expo/devcert": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", - "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.2.tgz", + "integrity": "sha512-wz1YedXwdNbYqk/PRr34vixnuw1+l6uSwmPXUj649cot5iYNccb2zIOnK7Nonb1Pfd/dxAY6CvU6KM3uSdxC7A==", "license": "MIT", "dependencies": { "@expo/sudo-prompt": "^9.3.1", @@ -2171,9 +2173,9 @@ "license": "MIT" }, "node_modules/@expo/ui": { - "version": "57.0.19", - "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.19.tgz", - "integrity": "sha512-WB95zQPge5nb04zgY3O2NmMtxg6G+qFH3yOAHcBkTUrJXoBReQoyY3cfp/8N5Q/azcfHJcBE3Jy0Rwq9OoRrfA==", + "version": "57.0.21", + "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-57.0.21.tgz", + "integrity": "sha512-4s1ypGNxJhJv2r1fj3wZo4Aubao0zGnCajCuiaTp8pTNrT+1vR8KvyoFEwp+c7gpKm2D58nGCiGBXhEnmKHtDQ==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.1.0", @@ -2200,19 +2202,19 @@ } }, "node_modules/@expo/ws-tunnel": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz", - "integrity": "sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.1.tgz", + "integrity": "sha512-mljsRffbOHzmSimYQbQu9h9NR3alwCCwEEouRGFL4QRTt8dNvsNtKEPa/5jHmtDLDtsh4xXsWKwzjN9BTpeXHQ==", "license": "MIT", "peerDependencies": { "ws": "^8.0.0" } }, "node_modules/@expo/xcpretty": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.5.tgz", - "integrity": "sha512-J3eL4n4h5QTwfD0SIz8OIk6/+sOL/hFZAMacgCM07UNlxBQfJipEpIC2AQxvGkbYeStByJ0TVhQAJo+DeNgaSQ==", - "license": "BSD-3-Clause", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.6.tgz", + "integrity": "sha512-uRaZZ/IsX1WkFW69xxTZtMeRvI5ziD9yoian+FMqdhITkU6NGvIWd5uRMh2Gbo/6xstpQ4v0GtF1V5YatTwzDA==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", "chalk": "^4.1.0", @@ -3671,9 +3673,9 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", - "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.5.tgz", + "integrity": "sha512-/NlO/rOqQabmg+GGr86vCmbJVhqTv85NfM9g6Gknf/JVygXeZ6e6HM/RW1omerW9HsFtecwNqgEAKV6UqDsvpA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3719,9 +3721,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", - "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.4.0.tgz", + "integrity": "sha512-IuMVHZ+Ah5fkctu3X3qeFrV/ZrBcuWuDTFUUPfS/L5hvoTjzhp/HdWGE14CsQombPdEidsa33HVlYZXi49PZ1Q==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" @@ -4673,6 +4675,20 @@ "node": ">= 10" } }, + "node_modules/@trebla/managed-webview": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trebla/managed-webview/-/managed-webview-0.2.0.tgz", + "integrity": "sha512-4KzAjhdGTeZ8E/SUdvWXlkKj1vVSj/ACkFT3FU6yUZStGp95cv1989xHL0tGe5KlsvJcir5x8ql2U/uRgBZqcg==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": ">=19 <20", + "react-native": ">=0.86 <1", + "react-native-webview": ">=13.16 <14" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -6125,9 +6141,9 @@ } }, "node_modules/babel-plugin-react-native-web": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", - "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.3.tgz", + "integrity": "sha512-e4fG8NbyRvk5GwU/7G2U9WxzPYtpC4IQN28AyRDzKE0kvywgohq8vT2x0/xBfg8a0RfSkXLlHAtUAGlSy3VhAw==", "license": "MIT" }, "node_modules/babel-plugin-syntax-hermes-parser": { @@ -6191,9 +6207,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "57.0.12", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.12.tgz", - "integrity": "sha512-rrZqJPYbVevy7rqibJZQFd3AsleGslJ63vmB3Qs8b5poOO5AHaehUGbZHjb0HoOZO59d86BEifYbq0/ncoM2UQ==", + "version": "57.0.13", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-57.0.13.tgz", + "integrity": "sha512-OOl3lqNQ4IagJfsNgtxuna0wpTWt1BswdyktGTOJ5EJCgdQUk/B2vaj/jsl03+kMyuIvpxKioVtbTzxh3mdvvQ==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -6242,7 +6258,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^57.0.19", + "expo-widgets": "^57.0.21", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -8338,13 +8354,13 @@ "license": "MIT" }, "node_modules/expo": { - "version": "57.0.24", - "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.24.tgz", - "integrity": "sha512-LxSnKMv7en/w4XDNxW3VOx7SbtQ2Bs4MyGhtoZCToDsEsBPO7pJXlv5GnQUCd7Wi8118QV/VDwsDLBKTB3Z3iQ==", + "version": "57.0.26", + "resolved": "https://registry.npmjs.org/expo/-/expo-57.0.26.tgz", + "integrity": "sha512-U1rsFwcuZNnU61Z/YLnnVC/ZqP0wpxh/rCKUMff3ot/rhLpxFY1zxEqu4vyaexyvBAB1mwpl9lA/ltcHBoQGyg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^57.0.26", + "@expo/cli": "^57.0.27", "@expo/config": "~57.0.9", "@expo/config-plugins": "~57.0.9", "@expo/devtools": "~57.0.1", @@ -8355,14 +8371,14 @@ "@expo/metro": "~56.0.2", "@expo/metro-config": "~57.0.12", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~57.0.12", + "babel-preset-expo": "~57.0.13", "expo-asset": "~57.0.18", - "expo-constants": "~57.0.19", + "expo-constants": "~57.0.20", "expo-file-system": "~57.0.7", "expo-font": "~57.0.4", "expo-keep-awake": "~57.0.2", "expo-modules-autolinking": "~57.0.13", - "expo-modules-core": "~57.0.18", + "expo-modules-core": "~57.0.20", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -8424,9 +8440,9 @@ } }, "node_modules/expo-constants": { - "version": "57.0.19", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.19.tgz", - "integrity": "sha512-3eK1kZRwCdMizQLlM/PPlPwjaPnB5qKvdRDMBMnB1ZxmvWJBf9TB4uEO4DMMfIPaFUxYFs29dsR5fPr1gh3FTQ==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.20.tgz", + "integrity": "sha512-WycPVvBgbluSa6yAh6DKnX646ZHrae0gV2ZLum5fioNkR+DiWyulq1+UfS++Jc6yfAh/l55NOirv6mt5cnQhSw==", "license": "MIT", "dependencies": { "@expo/env": "~2.4.3" @@ -8527,9 +8543,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "57.0.3", - "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-57.0.3.tgz", - "integrity": "sha512-aTh2J5eCxf+rSRMBJTnP8Ib5/k8DYFVDuEX1xqesN9F9aqTXocwMv/aQRVBNUs/iogDWhHIJz2pN97IsrrTcoA==", + "version": "57.0.4", + "resolved": "https://registry.npmjs.org/expo-glass-effect/-/expo-glass-effect-57.0.4.tgz", + "integrity": "sha512-/AG2Kljbqfgxu+GchI2YFIbxiF9cPsIpCy4iNDzlRiZk9rvR6BwRfB6gfQVu3V2ExaoGf2h5IsEnqVI5MYNBAg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -8563,12 +8579,12 @@ } }, "node_modules/expo-linking": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.10.tgz", - "integrity": "sha512-RzRi4l4pIYzjgoCJKnqkO+lUI+M2YX6qgcMIQkIbLAXdcWrykBoKYwHGlmAkdFHQm89GaYcq329yfxG+c1hv8w==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-57.0.11.tgz", + "integrity": "sha512-bfTyiPb8szacs8XkGowXt5ssH/vbF26vFG8WUWQar8CcRpQcIzXY980ZkP7pK7Pcdy5MattwFdujrWOxwv5s3Q==", "license": "MIT", "dependencies": { - "expo-constants": "~57.0.18", + "expo-constants": "~57.0.19", "invariant": "^2.2.4" }, "peerDependencies": { @@ -8617,13 +8633,13 @@ } }, "node_modules/expo-modules-core": { - "version": "57.0.18", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.18.tgz", - "integrity": "sha512-ovB7C+GVQTlKAOIsOIHJVVXN9Mn0ljf2SYBS+afrRaGqq+HZEn1D3o+HReIVeH1NjCn/xDBb7I4HjldHs6luZA==", + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-57.0.20.tgz", + "integrity": "sha512-vEL3aeih32yuD7sQHB3Y3LKXRT8mMgoVQWFIcHtX811Bl0StnNZUG2vT5je4fe+OWYVfkjb6gNqfMc04moREgA==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.6.1", - "expo-modules-jsi": "~57.1.0", + "expo-modules-jsi": "~57.1.1", "invariant": "^2.2.4" }, "peerDependencies": { @@ -8638,24 +8654,24 @@ } }, "node_modules/expo-modules-jsi": { - "version": "57.1.0", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.0.tgz", - "integrity": "sha512-a5ckeHfnbYfonhcHGkM0EU/a0Keh+/OufXy/HyFS+spr5Ib0N4Oh1ZsSYFQhp5xdOUFkVofKRhfH+VmcccIIpA==", + "version": "57.1.1", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-57.1.1.tgz", + "integrity": "sha512-ssnwz3ZuIgCbZj5A3J6NlhQrXZojg+BjkeWrOWqVJhyMHlV/A/cRT7s986chKku97BFt6ucmDu6/BCzrV45Wcw==", "license": "MIT", "peerDependencies": { "react-native": "*" } }, "node_modules/expo-router": { - "version": "57.0.22", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.22.tgz", - "integrity": "sha512-kOuS3RjMC73YLyBFiV9BzO0Z5OK1nLhPwmptK2II+tePtWXH7oA4zWaPwdWsdzTJRnmGMs/8Sr8YdTPHm1LUCg==", + "version": "57.0.24", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-57.0.24.tgz", + "integrity": "sha512-XpqE4Oh4yrmc+sVfsDQPLErBlus//85ulLRghVZltYcdvFm9yNzErmaKrXuBVMUL6D3tnpDtkIgxESG9hYBd7w==", "license": "MIT", "dependencies": { "@expo/log-box": "^57.0.4", "@expo/metro-runtime": "^57.0.16", "@expo/schema-utils": "^57.0.2", - "@expo/ui": "^57.0.19", + "@expo/ui": "^57.0.21", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -8663,7 +8679,7 @@ "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "expo-glass-effect": "^57.0.3", + "expo-glass-effect": "^57.0.4", "expo-server": "^57.0.3", "expo-symbols": "^57.0.3", "fast-deep-equal": "^3.1.3", @@ -8685,8 +8701,8 @@ "@expo/metro-runtime": "^57.0.16", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^57.0.19", - "expo-linking": "^57.0.10", + "expo-constants": "^57.0.20", + "expo-linking": "^57.0.11", "react": "*", "react-dom": "*", "react-native": "*", @@ -8719,15 +8735,15 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-collection": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", - "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.16.tgz", + "integrity": "sha512-yEQrjfI5eBbeYEJXOASpm+gs9eNHY8+UjkZ81GzcuwdLrSMOlEM8jnGoGdeGo92oiBMkuwZUhHTsnsrDiJLcyA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3" + "@radix-ui/react-primitive": "2.1.11", + "@radix-ui/react-slot": "1.4.0" }, "peerDependencies": { "@types/react": "*", @@ -8745,9 +8761,9 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-presence": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", - "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -8768,12 +8784,12 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-primitive": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", - "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.11.tgz", + "integrity": "sha512-RzcaSw5aKl0OPhBJ1wbolKSF6nVvN+0o/QLeIVq+09npiPdnCkPggy2k6gI6yetxl4ZyrlBPYFgCB8BQJZwXcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.3" + "@radix-ui/react-slot": "1.4.0" }, "peerDependencies": { "@types/react": "*", @@ -8791,18 +8807,18 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", - "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.20.tgz", + "integrity": "sha512-+ZjbkU21fTSJdn4nq/IFKUdKS+JSZHORBZ8ma1f6YpOMNzRt0xf3sVAWVK/+ktHCajPy4HYDsyHXZIZmOMxzIA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", - "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-collection": "1.1.16", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-direction": "1.1.5", "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-primitive": "2.1.11", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", @@ -8824,18 +8840,18 @@ } }, "node_modules/expo-router/node_modules/@radix-ui/react-tabs": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", - "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.22.tgz", + "integrity": "sha512-GmVH+P3wx1ZPMuH7WajQzygZIbqgnsb5h8z4Ow3sQMuDqgcqISSBNom6MLVNJvfrkuJTtqfXc3ZolHjX+w0CHg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-direction": "1.1.5", "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.1.11", + "@radix-ui/react-roving-focus": "1.1.20", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { @@ -8933,9 +8949,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "57.0.26", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.26.tgz", - "integrity": "sha512-vm769xuEyHMToi3uY66JLzRA2GGK+3WzdH71HNn67FuVhry82dKTvMUoeHYtHiindyeieiWfEvHT8Kr1VyEVPQ==", + "version": "57.0.27", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-57.0.27.tgz", + "integrity": "sha512-Jauk4chxmpVG5ElrMLTCwjFP20jbYU7PB4l3LYV8j+4e/3HU5jdl7qTQ4eVy1AaDUE9Ia2NCQ5A9kar2V1gaLg==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", @@ -8955,7 +8971,7 @@ "@expo/plist": "^0.8.1", "@expo/prebuild-config": "^57.0.16", "@expo/require-utils": "^57.0.5", - "@expo/router-server": "^57.0.10", + "@expo/router-server": "^57.0.11", "@expo/schema-utils": "^57.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", @@ -9016,17 +9032,17 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "57.0.10", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.10.tgz", - "integrity": "sha512-8rGh6N5di7jzu7UC1SuIA1TWmVd+vBwOHwC7/1ww+FA6CcxcWyQlcPl39h2+Jpnul78CVzWpIjOcdKsZsp2zXw==", + "version": "57.0.11", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-57.0.11.tgz", + "integrity": "sha512-0QxbqEkceb1WDeu5VqNdnTPVHlm3U183F7Y1t5LMohmGRAkHVL0PB4tCHBvEqKpLzOzHVObw3reGkJ1tX48h8Q==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^57.0.15", + "@expo/metro-runtime": "^57.0.16", "expo": "*", - "expo-constants": "^57.0.18", + "expo-constants": "^57.0.19", "expo-font": "^57.0.4", "expo-router": "*", "expo-server": "^57.0.3", @@ -15009,9 +15025,9 @@ } }, "node_modules/react-native-drawer-layout": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.10.tgz", - "integrity": "sha512-O6TQdZ5LSm3dqnuR4rX9KPtE+9dVg7jsezEYz1l01rkQTe4fQvYZIoPu2sZFl5X2N70uhRdjnPULySVR3sBUwA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.11.tgz", + "integrity": "sha512-Aua+LAZJ6L3v7ESHsNtxN2sO+hFQKsSz3EDKOLIpWni6PdQEJGFT0Mc2UnEsvFdfV743aHfODTAaHxsJ2D0uFw==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -15149,6 +15165,20 @@ "react-native": "*" } }, + "node_modules/react-native-webview": { + "version": "13.16.1", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz", + "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-worklets": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz", @@ -17396,22 +17426,22 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-dialog": { - "version": "1.1.23", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", - "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.2.0.tgz", + "integrity": "sha512-l5CX8HdNL+9cc4s7HkNyplumSA3FIx68sg9dttBHKr286fRBwq1MBniDVTeI/tkof92/APntMQvC1YG1Khb0Cg==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", - "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-dismissable-layer": "1.1.20", "@radix-ui/react-focus-guards": "1.1.6", - "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-focus-scope": "1.2.0", "@radix-ui/react-id": "1.1.4", - "@radix-ui/react-portal": "1.1.17", - "@radix-ui/react-presence": "1.1.10", - "@radix-ui/react-primitive": "2.1.10", - "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-portal": "1.1.18", + "@radix-ui/react-presence": "1.1.11", + "@radix-ui/react-primitive": "2.1.11", + "@radix-ui/react-slot": "1.4.0", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", @@ -17433,14 +17463,14 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", - "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.20.tgz", + "integrity": "sha512-35WHRrycfnj2m4LvPiKMKHGLM3Y2WvUA744CkRdxjs1BGDX1c8PVMfmiSGTUwnHRbqdtIoxveYTXvcJSt7VfgQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-primitive": "2.1.11", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, @@ -17460,13 +17490,13 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", - "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.2.0.tgz", + "integrity": "sha512-I5S7vsAOozx2lWSBAyuXjWIJ/A8qx4fjbv6115WoaRYQSb1yZDaeNHonUAQzXyE8oD+0whJHbQRl4HIE2gQ0Xw==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", - "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-primitive": "2.1.11", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { @@ -17485,12 +17515,12 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-portal": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", - "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.18.tgz", + "integrity": "sha512-ygFIJ+QXNKf72AAq8x3hd22+ws7pYpmc1S0E4kW0Je+qD4qEb3FvxeKVQRGrVDMOkQaYfsB+Qs+KDh81a1gAOw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-primitive": "2.1.11", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { @@ -17509,9 +17539,9 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-presence": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", - "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz", + "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==", "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" @@ -17532,12 +17562,12 @@ } }, "node_modules/vaul/node_modules/@radix-ui/react-primitive": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", - "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.11.tgz", + "integrity": "sha512-RzcaSw5aKl0OPhBJ1wbolKSF6nVvN+0o/QLeIVq+09npiPdnCkPggy2k6gI6yetxl4ZyrlBPYFgCB8BQJZwXcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.3" + "@radix-ui/react-slot": "1.4.0" }, "peerDependencies": { "@types/react": "*", diff --git a/package.json b/package.json index cacc91c..c024127 100644 --- a/package.json +++ b/package.json @@ -41,15 +41,16 @@ "@react-native-firebase/remote-config": "26.3.3", "@sentry/react-native": "~7.11.0", "@tanstack/react-query": "^5.102.8", - "expo": "~57.0.24", + "@trebla/managed-webview": "0.2.0", + "expo": "~57.0.26", "expo-application": "~57.0.3", - "expo-constants": "~57.0.19", + "expo-constants": "~57.0.20", "expo-dev-client": "~57.0.19", "expo-font": "~57.0.4", "expo-in-app-updates": "0.12.0", - "expo-linking": "~57.0.10", + "expo-linking": "~57.0.11", "expo-localization": "~57.0.2", - "expo-router": "~57.0.22", + "expo-router": "~57.0.24", "expo-splash-screen": "~57.0.9", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.4", @@ -66,6 +67,7 @@ "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.26.0", "react-native-svg": "15.15.4", + "react-native-webview": "13.16.1", "react-native-worklets": "0.10.1" }, "devDependencies": { diff --git a/src/app/routes/web-content.tsx b/src/app/routes/web-content.tsx new file mode 100644 index 0000000..65bfe8d --- /dev/null +++ b/src/app/routes/web-content.tsx @@ -0,0 +1 @@ +export { default } from "@/app/web-content"; diff --git a/src/app/web-content/index.tsx b/src/app/web-content/index.tsx new file mode 100644 index 0000000..f320573 --- /dev/null +++ b/src/app/web-content/index.tsx @@ -0,0 +1,143 @@ +import { + ManagedWebView, + type ManagedWebViewErrorContext, + type ManagedWebViewLoadingContext, +} from "@trebla/managed-webview"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { ArrowLeft } from "lucide-react-native"; +import { useEffect } from "react"; +import { ActivityIndicator, Pressable, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +import { useLocale } from "@/contexts/locale"; +import { useAppTheme } from "@/contexts/theme"; +import { openHttpsUrl } from "@/services/external-actions"; + +import { getLegalContent, parseLegalContentKind } from "./legal-content"; + +const OPENINGS_ORIGINS = ["https://openings.dev"] as const; +const WEB_VIEW_STYLE = { flex: 1 } as const; + +function OpeningsLoading({ + appearance, + labels, +}: ManagedWebViewLoadingContext): React.ReactNode { + return ( + + + + {labels.loading} + + + ); +} + +function OpeningsFailure({ + labels, + openExternal, + retry, +}: ManagedWebViewErrorContext): React.ReactNode { + return ( + + + {labels.errorTitle} + + + {labels.errorDescription} + + {retry ? ( + + + {labels.retry} + + + ) : null} + {openExternal ? ( + void openExternal()} + > + + {labels.openExternal} + + + ) : null} + + ); +} + +export default function WebContentScreen(): React.ReactNode { + const params = useLocalSearchParams<{ kind?: string | string[] }>(); + const router = useRouter(); + const { messages } = useLocale(); + const { theme } = useAppTheme(); + const kind = parseLegalContentKind(params.kind); + + useEffect(() => { + if (!kind) router.back(); + }, [kind, router]); + + if (!kind) return null; + + const document = getLegalContent(kind); + + return ( + + + router.back()} + > + + + + {messages.legal[kind]} + + + + + ); +} diff --git a/src/app/web-content/legal-content.ts b/src/app/web-content/legal-content.ts new file mode 100644 index 0000000..80e7e3b --- /dev/null +++ b/src/app/web-content/legal-content.ts @@ -0,0 +1,25 @@ +export type LegalContentKind = "privacy" | "terms"; + +const LEGAL_CONTENT = { + privacy: { + kind: "privacy", + path: "/privacy", + uri: "https://openings.dev/privacy", + }, + terms: { + kind: "terms", + path: "/terms", + uri: "https://openings.dev/terms", + }, +} as const satisfies Record< + LegalContentKind, + { kind: LegalContentKind; path: string; uri: string } +>; + +export function parseLegalContentKind(value: unknown): LegalContentKind | null { + return value === "privacy" || value === "terms" ? value : null; +} + +export function getLegalContent(kind: LegalContentKind) { + return LEGAL_CONTENT[kind]; +} diff --git a/src/components/app-drawer/index.tsx b/src/components/app-drawer/index.tsx index 16e0793..bac0f56 100644 --- a/src/components/app-drawer/index.tsx +++ b/src/components/app-drawer/index.tsx @@ -4,9 +4,11 @@ import { BriefcaseBusiness, BellOff, ChevronUp, + FileLock2, Globe2, Moon, Sun, + ScrollText, UserRound, UsersRound, X, @@ -143,6 +145,38 @@ export function AppDrawer({ onClose, visible }: AppDrawerProps): React.ReactNode style={{ paddingBottom: insets.bottom + DRAWER_FOOTER_PADDING }} testID="app-drawer-footer" > + + {([ + { icon: FileLock2, kind: "privacy", label: messages.legal.privacy }, + { icon: ScrollText, kind: "terms", label: messages.legal.terms }, + ] as const).map((item) => { + const Icon = item.icon; + + return ( + { + router.push({ + pathname: "/web-content", + params: { kind: item.kind }, + }); + close(); + }} + > + + + {item.label} + + + ); + })} + {notificationConsent === "granted" ? ( { + it.each([ + ["privacy", "https://openings.dev/privacy", "/privacy"], + ["terms", "https://openings.dev/terms", "/terms"], + ] as const)("maps %s to its canonical first-party document", (kind, uri, path) => { + expect(getLegalContent(kind)).toEqual({ kind, path, uri }); + }); + + it.each([undefined, "", "https://evil.example", "support", ["privacy"]])( + "rejects the route parameter %p", + (value) => expect(parseLegalContentKind(value)).toBeNull(), + ); + + it.each(["privacy", "terms"] as const)("accepts %s", (value) => { + expect(parseLegalContentKind(value)).toBe(value); + }); +}); diff --git a/tests/app/web-content-screen.test.tsx b/tests/app/web-content-screen.test.tsx new file mode 100644 index 0000000..6a5004f --- /dev/null +++ b/tests/app/web-content-screen.test.tsx @@ -0,0 +1,99 @@ +import { act, render } from "@testing-library/react-native"; + +import WebContentScreen from "@/app/web-content"; +import { messages } from "@/i18n/messages"; + +const mockBack = jest.fn(); +const mockOpenHttpsUrl = jest.fn(); +const mockMessages = messages; +let mockKind: unknown = "privacy"; +let managedProps: Record | undefined; + +jest.mock("expo-router", () => ({ + useLocalSearchParams: () => ({ kind: mockKind }), + useRouter: () => ({ back: mockBack }), +})); + +jest.mock("@/contexts/locale", () => ({ + useLocale: () => ({ messages: mockMessages.en }), +})); + +jest.mock("@/contexts/theme", () => ({ + useAppTheme: () => ({ + theme: { + colors: { + canvas: "#ffffff", + foreground: "#112233", + "muted-foreground": "#445566", + primary: "#aabbcc", + "primary-foreground": "#001122", + }, + }, + }), +})); + +jest.mock("@/services/external-actions", () => ({ + openHttpsUrl: (url: string) => mockOpenHttpsUrl(url), +})); + +jest.mock("@trebla/managed-webview", () => ({ + ManagedWebView: (props: Record) => { + const { View } = jest.requireActual("react-native"); + managedProps = props as Record; + return ; + }, +})); + +describe("WebContentScreen", () => { + beforeEach(() => { + mockKind = "privacy"; + managedProps = undefined; + mockBack.mockReset(); + mockOpenHttpsUrl.mockReset().mockResolvedValue(undefined); + }); + + it("loads only the selected first-party legal document", async () => { + const screen = await render(); + + expect(screen.getByTestId("managed-webview")).toBeTruthy(); + expect(screen.getByTestId("web-content-safe-area").props.edges).toEqual({ + top: "additive", + bottom: "additive", + left: "additive", + right: "additive", + }); + expect(managedProps).toMatchObject({ + uri: "https://openings.dev/privacy", + allowedOrigins: ["https://openings.dev"], + allowedPaths: ["/privacy"], + challengeDetection: true, + readiness: { type: "load" }, + timeoutMs: 10_000, + webViewStyle: { flex: 1 }, + labels: { + errorDescription: messages.en.legal.errorDescription, + errorTitle: messages.en.legal.errorTitle, + loading: messages.en.legal.loading, + openExternal: messages.en.legal.openBrowser, + retry: messages.en.legal.retry, + }, + }); + }); + + it("rejects arbitrary route values without mounting web content", async () => { + mockKind = "https://evil.example"; + const screen = await render(); + + expect(screen.queryByTestId("managed-webview")).toBeNull(); + expect(mockBack).toHaveBeenCalledTimes(1); + }); + + it("opens package-validated fallback URLs through the HTTPS service", async () => { + await render(); + const onOpenExternal = managedProps?.onOpenExternal as (url: string) => Promise; + + await act(() => onOpenExternal("https://docs.example.org/legal")); + + expect(mockOpenHttpsUrl).toHaveBeenCalledWith("https://docs.example.org/legal"); + }); +}); diff --git a/tests/components/app-header.test.tsx b/tests/components/app-header.test.tsx index 98de2c6..2b0372f 100644 --- a/tests/components/app-header.test.tsx +++ b/tests/components/app-header.test.tsx @@ -9,10 +9,11 @@ import { ThemeProvider } from "@/contexts/theme"; import { resetNotificationConsentForTests } from "@/services/notifications/consent"; const mockReplace = jest.fn(); +const mockPush = jest.fn(); const mockWithdrawNotifications = jest.fn().mockResolvedValue(true); jest.mock("expo-router", () => ({ - useRouter: () => ({ replace: mockReplace }), + useRouter: () => ({ push: mockPush, replace: mockReplace }), })); jest.mock("@/services/notifications/preferences", () => ({ @@ -74,6 +75,8 @@ describe("AppHeader", () => { expect(screen.getByText("Authors")).toBeTruthy(); expect(screen.getByText("Support openings.dev")).toBeTruthy(); expect(screen.getByText("Star on GitHub")).toBeTruthy(); + expect(screen.getByText("Privacy policy")).toBeTruthy(); + expect(screen.getByText("Terms of service")).toBeTruthy(); expect(screen.getByLabelText("Language")).toBeTruthy(); expect(screen.getByLabelText("Appearance")).toBeTruthy(); @@ -90,6 +93,18 @@ describe("AppHeader", () => { expect(screen.getByText("Escuro")).toBeTruthy(); }); + it("opens legal documents inside the managed content route", async () => { + const screen = await renderAppHeader(); + + await fireEvent.press(await screen.findByLabelText("Open navigation menu")); + await fireEvent.press(screen.getByText("Privacy policy")); + + expect(mockPush).toHaveBeenCalledWith({ + pathname: "/web-content", + params: { kind: "privacy" }, + }); + }); + it("uses the wordmark as a native destination back to Jobs", async () => { const screen = await renderAppHeader(); diff --git a/tests/contracts/app-config.test.ts b/tests/contracts/app-config.test.ts index 54e909b..ac0347c 100644 --- a/tests/contracts/app-config.test.ts +++ b/tests/contracts/app-config.test.ts @@ -98,14 +98,14 @@ describe("native application identity", () => { ) as { dependencies: Record }; expect(manifest.dependencies).toMatchObject({ - expo: "~57.0.24", + expo: "~57.0.26", "expo-application": "~57.0.3", - "expo-constants": "~57.0.19", + "expo-constants": "~57.0.20", "expo-dev-client": "~57.0.19", "expo-font": "~57.0.4", - "expo-linking": "~57.0.10", + "expo-linking": "~57.0.11", "expo-localization": "~57.0.2", - "expo-router": "~57.0.22", + "expo-router": "~57.0.24", "expo-splash-screen": "~57.0.9", "expo-system-ui": "~57.0.4", }); diff --git a/tests/contracts/shared-packages.test.ts b/tests/contracts/shared-packages.test.ts index 3122573..653eba6 100644 --- a/tests/contracts/shared-packages.test.ts +++ b/tests/contracts/shared-packages.test.ts @@ -41,4 +41,13 @@ describe("shared package integration", () => { expect(darkRuntimeTheme.colors.canvas).toBe("#0D1211"); expect(nativewindPreset.theme.extend.spacing.touch).toBe(44); }); + + it("pins the public managed WebView and its native peer", () => { + const manifest = JSON.parse( + readFileSync(path.join(projectRoot, "package.json"), "utf8"), + ); + + expect(manifest.dependencies["@trebla/managed-webview"]).toBe("0.2.0"); + expect(manifest.dependencies["react-native-webview"]).toBe("13.16.1"); + }); }); diff --git a/tests/i18n/messages.test.ts b/tests/i18n/messages.test.ts index 07666f3..cc7be5d 100644 --- a/tests/i18n/messages.test.ts +++ b/tests/i18n/messages.test.ts @@ -81,4 +81,23 @@ describe("localized messages", () => { }); } }); + + it("provides complete legal content copy", () => { + const messages = moduleContract.messages as Record< + string, + { legal?: Record } + >; + + for (const locale of SUPPORTED_LOCALES) { + expect(messages[locale]?.legal).toEqual({ + errorDescription: expect.any(String), + errorTitle: expect.any(String), + loading: expect.any(String), + openBrowser: expect.any(String), + privacy: expect.any(String), + retry: expect.any(String), + terms: expect.any(String), + }); + } + }); });