diff --git a/packages/special-pages/index.mjs b/packages/special-pages/index.mjs index 07d194323..c3a83943f 100644 --- a/packages/special-pages/index.mjs +++ b/packages/special-pages/index.mjs @@ -36,11 +36,6 @@ export const support = { 'apple': ['copy', 'inline-html'], }, /** @type {Partial>} */ - sslerrorpage: { - 'integration': ['copy', 'build-js'], - 'apple': ['copy', 'build-js', 'inline-html'], - }, - /** @type {Partial>} */ onboarding: { 'integration': ['copy', 'build-js'], 'windows': ['copy', 'build-js'], @@ -55,6 +50,11 @@ export const support = { 'integration': ['copy', 'build-js'], 'apple': ['copy', 'build-js'], }, + /** @type {Partial>} */ + 'special-error': { + 'integration': ['copy', 'build-js'], + 'apple': ['copy', 'build-js', 'inline-html'], + }, } /** @type {{src: string, dest: string, injectName: string}[]} */ diff --git a/packages/special-pages/messages/sslerrorpage/leaveSite.notify.json b/packages/special-pages/messages/special-error/advancedInfo.notify.json similarity index 100% rename from packages/special-pages/messages/sslerrorpage/leaveSite.notify.json rename to packages/special-pages/messages/special-error/advancedInfo.notify.json diff --git a/packages/special-pages/messages/sslerrorpage/visitSite.notify.json b/packages/special-pages/messages/special-error/initialSetup.request.json similarity index 100% rename from packages/special-pages/messages/sslerrorpage/visitSite.notify.json rename to packages/special-pages/messages/special-error/initialSetup.request.json diff --git a/packages/special-pages/messages/special-error/initialSetup.response.json b/packages/special-pages/messages/special-error/initialSetup.response.json new file mode 100644 index 000000000..26d5e35b3 --- /dev/null +++ b/packages/special-pages/messages/special-error/initialSetup.response.json @@ -0,0 +1,115 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["locale", "env", "platform", "errorData"], + "properties": { + "locale": { + "type": "string" + }, + "env": { + "type": "string", + "enum": ["development", "production"] + }, + "platform": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "enum": ["macos", "windows", "android", "ios"] + } + } + }, + "errorData": { + "type": "object", + "oneOf": [ + { + "type": "object", + "title": "Phishing", + "required": ["kind"], + "properties": { + "kind": { + "type": "string", + "const": "phishing" + } + } + }, { + "type": "object", + "title": "SSL Expired Certificate", + "required": ["kind", "errorType", "domain"], + "properties": { + "kind": { + "type": "string", + "const": "ssl" + }, + "errorType": { + "type": "string", + "const": "expired" + }, + "domain": { + "type": "string" + } + } + }, { + "type": "object", + "title": "SSL Invalid Certificate", + "required": ["kind", "errorType", "domain"], + "properties": { + "kind": { + "type": "string", + "const": "ssl" + }, + "errorType": { + "type": "string", + "const": "invalid" + }, + "domain": { + "type": "string" + } + } + }, { + "type": "object", + "title": "SSL Self Signed Certificate", + "required": ["kind", "errorType", "domain"], + "properties": { + "kind": { + "type": "string", + "const": "ssl" + }, + "errorType": { + "type": "string", + "const": "selfSigned" + }, + "domain": { + "type": "string" + } + } + }, { + "type": "object", + "title": "SSL Wrong Host", + "required": ["kind", "errorType", "domain", "eTldPlus1"], + "properties": { + "kind": { + "type": "string", + "const": "ssl" + }, + "errorType": { + "type": "string", + "const": "wrongHost" + }, + "domain": { + "type": "string" + }, + "eTldPlus1": { + "type": "string" + } + } + } + ] + }, + "localeStrings": { + "type": "string", + "description": "Optional locale-specific strings" + } + } +} diff --git a/packages/special-pages/messages/special-error/leaveSite.notify.json b/packages/special-pages/messages/special-error/leaveSite.notify.json new file mode 100644 index 000000000..0af74a319 --- /dev/null +++ b/packages/special-pages/messages/special-error/leaveSite.notify.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/packages/special-pages/messages/special-error/reportInitException.notify.json b/packages/special-pages/messages/special-error/reportInitException.notify.json new file mode 100644 index 000000000..afd7d6bde --- /dev/null +++ b/packages/special-pages/messages/special-error/reportInitException.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } +} diff --git a/packages/special-pages/messages/special-error/reportPageException.notify.json b/packages/special-pages/messages/special-error/reportPageException.notify.json new file mode 100644 index 000000000..afd7d6bde --- /dev/null +++ b/packages/special-pages/messages/special-error/reportPageException.notify.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + } + } +} diff --git a/packages/special-pages/messages/special-error/visitSite.notify.json b/packages/special-pages/messages/special-error/visitSite.notify.json new file mode 100644 index 000000000..0af74a319 --- /dev/null +++ b/packages/special-pages/messages/special-error/visitSite.notify.json @@ -0,0 +1,3 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#" +} diff --git a/packages/special-pages/pages/example/app/index.js b/packages/special-pages/pages/example/app/index.js index 5f4b1b22f..eb485821d 100644 --- a/packages/special-pages/pages/example/app/index.js +++ b/packages/special-pages/pages/example/app/index.js @@ -1,4 +1,3 @@ -import { callWithRetry } from '../../../shared/call-with-retry.js' import { h, render } from 'preact' import { EnvironmentProvider, UpdateEnvironment } from '../../../shared/components/EnvironmentProvider.js' @@ -7,6 +6,9 @@ import { Components } from './components/Components.jsx' import enStrings from '../src/locales/en/example.json' import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js' +import { callWithRetry } from '../../../shared/call-with-retry.js' + +import '../../../shared/styles/global.css' // global styles /** * @param {import("../src/js/index.js").ExamplePage} messaging diff --git a/packages/special-pages/pages/example/src/js/index.js b/packages/special-pages/pages/example/src/js/index.js index a49376917..ccda21dbf 100644 --- a/packages/special-pages/pages/example/src/js/index.js +++ b/packages/special-pages/pages/example/src/js/index.js @@ -1,3 +1,11 @@ +/** + * @module Example Page + * @category Special Pages + * + * @description + * Special Page example. Used as a template for new special pages. + */ + import { createTypedMessages } from '@duckduckgo/messaging' import { Environment } from '../../../../shared/environment.js' import { createSpecialPageMessaging } from '../../../../shared/create-special-page-messaging.js' @@ -12,8 +20,17 @@ export class ExamplePage { } /** - * This will be sent if the application has loaded, but a client-side error - * has occurred that cannot be recovered from + * Sends an initial message to the native layer. This is the opportunity for the native layer + * to provide the initial state of the application or any configuration, for example: + * + * ```json + * { + * "env": "development", + * "locale": "en" + * } + * ``` + * + * @returns {Promise} */ initialSetup () { return this.messaging.request('initialSetup') diff --git a/packages/special-pages/pages/release-notes/app/Components.js b/packages/special-pages/pages/release-notes/app/Components.js index d68117fb4..f473bac94 100644 --- a/packages/special-pages/pages/release-notes/app/Components.js +++ b/packages/special-pages/pages/release-notes/app/Components.js @@ -61,7 +61,7 @@ export function Components () {

Restart Button

- +

diff --git a/packages/special-pages/pages/special-error/app/components/AdvancedInfo.jsx b/packages/special-pages/pages/special-error/app/components/AdvancedInfo.jsx new file mode 100644 index 000000000..226ba87c9 --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/AdvancedInfo.jsx @@ -0,0 +1,69 @@ +import { h } from 'preact' +import { useRef, useEffect } from 'preact/hooks' +import { useTypedTranslation } from '../types' +import { Text } from '../../../../shared/components/Text/Text' +import { useMessaging } from '../providers/MessagingProvider' +import { useAdvancedInfoHeading, useAdvancedInfoContent } from '../hooks/ErrorStrings' + +import styles from './AdvancedInfo.module.css' + +function useScrollTarget() { + /** @type {import("preact/hooks").MutableRef} */ + const linkRef = useRef(null) + return { + ref: linkRef, + trigger: () => { + linkRef.current?.scrollIntoView({ behavior: 'smooth' }) + } + } +} + +/** + * @param {object} props + * @param {import("preact/hooks").MutableRef} [props.elemRef] + */ +export function VisitSiteLink({ elemRef }) { + const { t } = useTypedTranslation(); + const {messaging} = useMessaging(); + return ( + messaging?.visitSite()} ref={elemRef}> + {t('visitSiteButton')} + + ) +} + +export function AdvancedInfoHeading() { + const heading = useAdvancedInfoHeading() + + return ( +
+ {heading} +
+ ) +} + +export function AdvancedInfoContent() { + const content = useAdvancedInfoContent() + + return ( +
+ {content.map(text => {text})} +
+ ) +} + +export function AdvancedInfo() { + const { ref, trigger } = useScrollTarget(); + + return ( +
+
+ + + + + +
+
+ ) +} diff --git a/packages/special-pages/pages/special-error/app/components/AdvancedInfo.module.css b/packages/special-pages/pages/special-error/app/components/AdvancedInfo.module.css new file mode 100644 index 000000000..726b7c842 --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/AdvancedInfo.module.css @@ -0,0 +1,90 @@ +.container { + align-items: flex-start; + display: flex; + flex-flow: column; + gap: var(--sp-4); + max-width: var(--ios-content-max-width); + width: 100%; + + animation-duration: 300ms; + animation-fill-mode: forwards; + animation-name: appear; +} + +.content { + display: flex; + flex-flow: column; + gap: var(--sp-5); + + & a { + color: var(--link-color); + } +} + +.visitSite { + color: var(--visit-site-color); + cursor: pointer; + font-size: calc(13 * var(--px-in-rem)); + letter-spacing: calc(-0.08 * var(--px-in-rem)); + line-height: calc(16 * var(--px-in-rem)); + text-decoration: underline; +} + +@keyframes appear { + 0% { + padding: 0 var(--sp-10); + max-height: 0; + } + 100% { + padding: var(--sp-6) var(--sp-10); + max-height: calc(400 * var(--px-in-rem)); + } +} + +/* Platform-specific styles */ + +/* macOS */ +[data-platform-name="macos"] { + & .container { + background: var(--advanced-info-bg); + box-shadow: inset 0 1px 0 0 var(--border-color); + } + + & .visitSite { + align-self: flex-end; + } +} + +/* iOS */ +[data-platform-name="ios"] { + & .wrapper { + display: flex; + justify-content: center; + border-top: 1px solid var(--color-black-at-9); + width: 100%; + + @media (prefers-color-scheme: dark) { + border-top: 1px solid var(--color-white-at-9); + } + } + + & .container { + align-items: center; + gap: var(--sp-6); + } + + & .heading { + text-align: center; + } + + & .content { + text-align: center; + } + + & .visitSite { + font-size: calc(16 * var(--px-in-rem)); + font-weight: 400; + letter-spacing: calc(-0.31 * var(--px-in-rem)); + line-height: calc(21 * var(--px-in-rem)); + } +} diff --git a/packages/special-pages/pages/special-error/app/components/App.jsx b/packages/special-pages/pages/special-error/app/components/App.jsx new file mode 100644 index 000000000..e9de0b1d7 --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/App.jsx @@ -0,0 +1,57 @@ +import { h } from "preact"; +import { useState } from "preact/hooks"; +import { useEnv } from "../../../../shared/components/EnvironmentProvider"; +import { useMessaging } from "../providers/MessagingProvider"; +import { ErrorBoundary } from '../../../../shared/components/ErrorBoundary' +import { ErrorFallback } from "./ErrorFallback"; +import { Warning } from "./Warning"; +import { AdvancedInfo } from "./AdvancedInfo"; + +import styles from "./App.module.css"; + +export function SpecialErrorView() { + const [advancedInfoVisible, setAdvancedInfoVisible] = useState(false) + const { messaging } = useMessaging() + + const advancedButtonHandler = () => { + messaging?.advancedInfo() + setAdvancedInfoVisible(true) + } + + return ( +
+ + { advancedInfoVisible && } +
+ ) +} + +export function App() { + const { messaging } = useMessaging() + + /** + * @param {Error} error + */ + function didCatch (error) { + const message = error?.message || 'unknown' + console.error('ErrorBoundary', message) + messaging?.reportPageException({ message }) + } + + return ( +
+ }> + + + +
+ ) +} + +export function WillThrow () { + const env = useEnv() + if (env.willThrow) { + throw new Error('Simulated Exception') + } + return null +} \ No newline at end of file diff --git a/packages/special-pages/pages/special-error/app/components/App.module.css b/packages/special-pages/pages/special-error/app/components/App.module.css new file mode 100644 index 000000000..b1e88a6fb --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/App.module.css @@ -0,0 +1,56 @@ +html, +body { + height: 100%; + margin: 0; +} + +.main { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + + + @media (max-height: 400px) { + padding-top: var(--sp-10); + align-items: flex-start; + } + + @media (max-height: 700px) { + [data-platform-name="ios"] & { + padding-top: var(--sp-10); + align-items: flex-start; + } + } +} + +.container { + display: flex; + flex-direction: column; + overflow: hidden; + width: 100%; +} + +/* Platform-specific styles */ + +/* macOS */ +[data-platform-name="macos"] { + & .container { + background: var(--container-bg); + border-radius: var(--sp-2); + border: 1px solid var(--border-color); + min-width: calc(400 * var(--px-in-rem)); + width: calc(504 * var(--px-in-rem)); + } +} + +/* iOS */ +[data-platform-name="ios"] { + & .container { + align-items: center; + } +} diff --git a/packages/special-pages/pages/special-error/app/components/Components.jsx b/packages/special-pages/pages/special-error/app/components/Components.jsx new file mode 100644 index 000000000..920b63f9c --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/Components.jsx @@ -0,0 +1,145 @@ +import { h } from "preact"; +import { usePlatformName } from "../providers/SettingsProvider"; +import { useErrorData } from "../providers/SpecialErrorProvider"; +import { Warning, WarningHeading, WarningContent, AdvancedInfoButton, LeaveSiteButton } from "./Warning"; +import { AdvancedInfo, AdvancedInfoHeading, AdvancedInfoContent, VisitSiteLink } from "./AdvancedInfo"; +import { SpecialErrorView } from "./App"; +import { sampleData } from "../../src/js/sampleData"; + +import styles from "./Components.module.css"; + +/** @type {Record, string>} */ +const platforms = { + 'macos': 'macOS', + 'ios': 'iOS' +} + +/** + * @param {import("../../../../types/special-error.js").InitialSetupResponse['errorData']} errorData + */ +function idForError(errorData) { + const { kind } = errorData + if (kind === 'phishing') { + return kind + } + + const { errorType } = errorData + return `${kind}.${errorType}` +} + +export function Components() { + const platformName = usePlatformName() + const errorData = useErrorData() + + const handlePlatformChange = (value) => { + if (Object.keys(platforms).includes(value)) { + const url = new URL(window.location.href) + url.searchParams.set('platform', value) + window.location.href = url.toString() + } + } + + const handleErrorTypeChange = (value) => { + if (Object.keys(sampleData).includes(value)) { + const url = new URL(window.location.href) + url.searchParams.set('errorId', value) + window.location.href = url.toString() + } + } + + return ( +
+
+
+ + +
+
+ + +
+
+
+

Special Error Components

+ +
+

Warning Heading

+
+ +
+
+ +
+

Warning Content

+
+ +
+
+ +
+

Advanced Info Heading

+
+ +
+
+ +
+

Advanced Info Content

+
+ +
+
+ +
+

Leave Site Button

+
+ +
+
+ +
+

Advanced Info Button

+
+ {}}/> +
+
+ +
+

Visit Site Link

+
+ +
+
+ +
+

Warning

+
+ {}}/> +
+
+ +
+

Advanced Info

+
+ +
+
+ +
+

Special Error View

+
+ +
+
+
+
+ ) +} diff --git a/packages/special-pages/pages/special-error/app/components/Components.module.css b/packages/special-pages/pages/special-error/app/components/Components.module.css new file mode 100644 index 000000000..4affa88c5 --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/Components.module.css @@ -0,0 +1,35 @@ +.selector { + width: 100%; + display: flex; + padding: var(--sp-5); + gap: var(--sp-4); + + & fieldset { + border: 0; + display: flex; + gap: var(--sp-2); + } +} + +.main { + display: flex; + flex-flow: column; + padding: var(--sp-5); + gap: var(--sp-5); + + & > section { + display: flex; + flex-flow: column; + border-top: 1px solid var(--theme-text-primary-color); + padding: var(--sp-3); + gap: var(--sp-3); + + & > h1 { + padding: var(--sp-3) 0; + } + + & > h2 { + padding: var(--sp-2) 0; + } + } +} diff --git a/packages/special-pages/pages/special-error/app/components/ErrorFallback.js b/packages/special-pages/pages/special-error/app/components/ErrorFallback.js new file mode 100644 index 000000000..37b6f78a7 --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/ErrorFallback.js @@ -0,0 +1,7 @@ +import { h } from 'preact' + +export function ErrorFallback () { + return ( +

Something went wrong

+ ) +} diff --git a/packages/special-pages/pages/special-error/app/components/Warning.jsx b/packages/special-pages/pages/special-error/app/components/Warning.jsx new file mode 100644 index 000000000..b4fd9cede --- /dev/null +++ b/packages/special-pages/pages/special-error/app/components/Warning.jsx @@ -0,0 +1,92 @@ +import { h } from 'preact' +import classNames from 'classnames' +import { useTypedTranslation } from '../types' +import { useMessaging } from '../providers/MessagingProvider' +import { useErrorData } from '../providers/SpecialErrorProvider' +import { usePlatformName } from '../providers/SettingsProvider' +import { useWarningHeading, useWarningContent } from '../hooks/ErrorStrings' +import { Text } from '../../../../shared/components/Text/Text' +import { Button } from '../../../../shared/components/Button/Button' + +import styles from './Warning.module.css' + +/** + * @param {object} props + * @param {import('preact').JSX.MouseEventHandler} props.onClick + */ +export function AdvancedInfoButton({ onClick }) { + const { t } = useTypedTranslation() + const platformName = usePlatformName() + + return ( + + ) +} + +export function LeaveSiteButton() { + const { t } = useTypedTranslation() + const { messaging } = useMessaging() + const platformName = usePlatformName() + + return ( + + ) +} + +export function WarningHeading() { + const { kind } = useErrorData() + const heading = useWarningHeading() + const platformName = usePlatformName() + + return ( +
+