-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NTP: FE - Freemium PIR Banner (#1315)
* feat: Create MessageBar component * feat: Use MessageBar in RMF * wip * chore: fix typing maybe? * chore: Add to page * chore: Undo the message bar creation * fix: compnents view * fix: Typing issue in NextStepsCards * fix: naming is hard * chore: Change PIRBanner message shape * chore: Move convertMarkdownToHTMLForStrongTags to util file * fix: naming * chore: Add tests for the markdown convert util * chore: Add Playwright tests * docs: Add PIR Banner docs * fix: test pause * fix: mocktransport * added xss test (#1330) Co-authored-by: Shane Osbourne <shane.osbourne8@gmail.com> * chore: Readd icon after rebase * path update --------- Co-authored-by: Shane Osbourne <shane.osbourne8@gmail.com>
- Loading branch information
1 parent
2bab275
commit 7ef4778
Showing
29 changed files
with
780 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
special-pages/pages/new-tab/app/entry-points/freemiumPIRBanner.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { h } from 'preact'; | ||
import { Centered } from '../components/Layout.js'; | ||
import { FreemiumPIRBannerConsumer } from '../freemium-pir-banner/components/FreemiumPIRBanner.js'; | ||
import { FreemiumPIRBannerProvider } from '../freemium-pir-banner/FreemiumPIRBannerProvider.js'; | ||
|
||
export function factory() { | ||
return ( | ||
<Centered data-entry-point="freemiumPIRBanner"> | ||
<FreemiumPIRBannerProvider> | ||
<FreemiumPIRBannerConsumer /> | ||
</FreemiumPIRBannerProvider> | ||
</Centered> | ||
); | ||
} |
94 changes: 94 additions & 0 deletions
94
special-pages/pages/new-tab/app/freemium-pir-banner/FreemiumPIRBannerProvider.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { createContext, h } from 'preact'; | ||
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks'; | ||
import { useMessaging } from '../types.js'; | ||
import { FreemiumPIRBannerService } from './freemiumPIRBanner.service.js'; | ||
import { reducer, useDataSubscription, useInitialData } from '../service.hooks.js'; | ||
|
||
/** | ||
* @typedef {import('../../types/new-tab.js').FreemiumPIRBannerData} FreemiumPIRBannerData | ||
* @typedef {import('../service.hooks.js').State<FreemiumPIRBannerData, undefined>} State | ||
* @typedef {import('../service.hooks.js').Events<FreemiumPIRBannerData, undefined>} Events | ||
*/ | ||
|
||
/** | ||
* These are the values exposed to consumers. | ||
*/ | ||
export const FreemiumPIRBannerContext = createContext({ | ||
/** @type {State} */ | ||
state: { status: 'idle', data: null, config: null }, | ||
/** @type {(id: string) => void} */ | ||
dismiss: (id) => { | ||
throw new Error('must implement dismiss' + id); | ||
}, | ||
/** @type {(id: string) => void} */ | ||
action: (id) => { | ||
throw new Error('must implement action' + id); | ||
}, | ||
}); | ||
|
||
export const FreemiumPIRBannerDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch<Events>} */ ({})); | ||
|
||
/** | ||
* A data provider that will use `FreemiumPIRBannerService` to fetch data, subscribe | ||
* to updates and modify state. | ||
* | ||
* @param {Object} props | ||
* @param {import("preact").ComponentChild} props.children | ||
*/ | ||
export function FreemiumPIRBannerProvider(props) { | ||
const initial = /** @type {State} */ ({ | ||
status: 'idle', | ||
data: null, | ||
config: null, | ||
}); | ||
|
||
// const [state, dispatch] = useReducer(withLog('FreemiumPIRBannerProvider', reducer), initial) | ||
const [state, dispatch] = useReducer(reducer, initial); | ||
|
||
// create an instance of `FreemiumPIRBannerService` for the lifespan of this component. | ||
const service = useService(); | ||
|
||
// get initial data | ||
useInitialData({ dispatch, service }); | ||
|
||
// subscribe to data updates | ||
useDataSubscription({ dispatch, service }); | ||
|
||
// todo(valerie): implement onDismiss in the service | ||
const dismiss = useCallback( | ||
(id) => { | ||
console.log('onDismiss'); | ||
service.current?.dismiss(id); | ||
}, | ||
[service], | ||
); | ||
|
||
const action = useCallback( | ||
(id) => { | ||
service.current?.action(id); | ||
}, | ||
[service], | ||
); | ||
|
||
return ( | ||
<FreemiumPIRBannerContext.Provider value={{ state, dismiss, action }}> | ||
<FreemiumPIRBannerDispatchContext.Provider value={dispatch}>{props.children}</FreemiumPIRBannerDispatchContext.Provider> | ||
</FreemiumPIRBannerContext.Provider> | ||
); | ||
} | ||
|
||
/** | ||
* @return {import("preact").RefObject<FreemiumPIRBannerService>} | ||
*/ | ||
export function useService() { | ||
const service = useRef(/** @type {FreemiumPIRBannerService|null} */ (null)); | ||
const ntp = useMessaging(); | ||
useEffect(() => { | ||
const stats = new FreemiumPIRBannerService(ntp); | ||
service.current = stats; | ||
return () => { | ||
stats.destroy(); | ||
}; | ||
}, [ntp]); | ||
return service; | ||
} |
27 changes: 27 additions & 0 deletions
27
special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.examples.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { h } from 'preact'; | ||
import { noop } from '../../utils.js'; | ||
import { FreemiumPIRBanner } from './FreemiumPIRBanner.js'; | ||
import { freemiumPIRDataExamples } from '../mocks/freemiumPIRBanner.data.js'; | ||
|
||
/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */ | ||
|
||
export const freemiumPIRBannerExamples = { | ||
'freemiumPIR.onboarding': { | ||
factory: () => ( | ||
<FreemiumPIRBanner | ||
message={freemiumPIRDataExamples.onboarding.content} | ||
dismiss={noop('freemiumPIRBanner_dismiss')} | ||
action={noop('freemiumPIRBanner_action')} | ||
/> | ||
), | ||
}, | ||
'freemiumPIR.scan_results': { | ||
factory: () => ( | ||
<FreemiumPIRBanner | ||
message={freemiumPIRDataExamples.scan_results.content} | ||
dismiss={noop('freemiumPIRBanner_dismiss')} | ||
action={noop('freemiumPIRBanner_action')} | ||
/> | ||
), | ||
}, | ||
}; |
48 changes: 48 additions & 0 deletions
48
special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import cn from 'classnames'; | ||
import { h } from 'preact'; | ||
import { Button } from '../../../../../shared/components/Button/Button'; | ||
import { DismissButton } from '../../components/DismissButton'; | ||
import styles from './FreemiumPIRBanner.module.css'; | ||
import { FreemiumPIRBannerContext } from '../FreemiumPIRBannerProvider'; | ||
import { useContext } from 'preact/hooks'; | ||
import { convertMarkdownToHTMLForStrongTags } from '../freemiumPIRBanner.utils'; | ||
|
||
/** | ||
* @typedef { import("../../../types/new-tab").FreemiumPIRBannerMessage} FreemiumPIRBannerMessage | ||
* @param {object} props | ||
* @param {FreemiumPIRBannerMessage} props.message | ||
* @param {(id: string) => void} props.dismiss | ||
* @param {(id: string) => void} props.action | ||
*/ | ||
|
||
export function FreemiumPIRBanner({ message, action, dismiss }) { | ||
const processedMessageDescription = convertMarkdownToHTMLForStrongTags(message.descriptionText); | ||
return ( | ||
<div id={message.id} class={cn(styles.root, styles.icon)}> | ||
<span class={styles.iconBlock}> | ||
<img src={`./icons/Information-Remover-96.svg`} alt="" /> | ||
</span> | ||
<div class={styles.content}> | ||
{message.titleText && <h2 class={styles.title}>{message.titleText}</h2>} | ||
<p class={styles.description} dangerouslySetInnerHTML={{ __html: processedMessageDescription }} /> | ||
</div> | ||
{message.messageType === 'big_single_action' && message?.actionText && action && ( | ||
<div class={styles.btnBlock}> | ||
<Button variant="standard" onClick={() => action(message.id)}> | ||
{message.actionText} | ||
</Button> | ||
</div> | ||
)} | ||
{message.id && dismiss && <DismissButton className={styles.dismissBtn} onClick={() => dismiss(message.id)} />} | ||
</div> | ||
); | ||
} | ||
|
||
export function FreemiumPIRBannerConsumer() { | ||
const { state, action, dismiss } = useContext(FreemiumPIRBannerContext); | ||
|
||
if (state.status === 'ready' && state.data.content) { | ||
return <FreemiumPIRBanner message={state.data.content} action={action} dismiss={dismiss} />; | ||
} | ||
return null; | ||
} |
77 changes: 77 additions & 0 deletions
77
special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
.root { | ||
--ntp-freemiumPIR-surface-background-color: rgba(0, 0, 0, .06); | ||
background: var(--ntp-freemiumPIR-surface-background-color); | ||
padding: calc(14 * var(--px-in-rem)) var(--sp-8) calc(14 * var(--px-in-rem)) var(--sp-4); | ||
border-radius: var(--border-radius-lg); | ||
position: relative; | ||
display: flex; | ||
justify-content: flex-start; | ||
align-items: flex-start; | ||
font-family: system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto; | ||
color: var(--ntp-text-normal); | ||
width: 100%; | ||
animation: animate-fade .2s cubic-bezier(0.55, 0.055, 0.666, 0.19); | ||
margin-bottom: var(--ntp-gap); | ||
|
||
&.icon { | ||
padding-left: var(--sp-2); | ||
} | ||
|
||
@media screen and (prefers-color-scheme: dark) { | ||
background-color: var(--color-white-at-6); | ||
} | ||
} | ||
|
||
.iconBlock { | ||
margin-right: var(--sp-2); | ||
width: 3rem; | ||
min-width: 3rem; | ||
} | ||
|
||
.content { | ||
flex-grow: 1; | ||
height: 100%; | ||
align-self: center; | ||
} | ||
|
||
.title { | ||
font-size: var(--body-font-size); | ||
font-weight: var(--title-2-font-weight); | ||
line-height: normal; | ||
margin-bottom: var(--sp-1); | ||
} | ||
|
||
.description { | ||
font-size: var(--body-font-size); | ||
line-height: var(--body-line-height); | ||
} | ||
|
||
.btnBlock { | ||
margin-left: var(--sp-3); | ||
align-self: center; | ||
} | ||
|
||
.btnRow { | ||
margin-top: var(--sp-3); | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: calc(10 * var(--px-in-rem)); | ||
} | ||
|
||
.dismissBtn { | ||
position: absolute; | ||
top: 0.5rem; | ||
right: 0.5rem; | ||
} | ||
|
||
|
||
@keyframes animate-fade { | ||
0% { | ||
opacity: 0; | ||
scale: 0.98; | ||
} | ||
100% { | ||
opacity: 1; | ||
scale: 1; | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
special-pages/pages/new-tab/app/freemium-pir-banner/freemium-pir-banner.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
--- | ||
title: Freemium PIR Banner | ||
--- | ||
|
||
## Requests: | ||
- {@link "NewTab Messages".FreemiumPIRBannerGetDataRequest `freemiumPIRBanner_getData`} | ||
- Used to fetch the initial data (during the first render) | ||
- returns {@link "NewTab Messages".FreemiumPIRBannerData} | ||
|
||
## Subscriptions: | ||
- {@link "NewTab Messages".FreemiumPIRBannerOnDataUpdateSubscription `freemiumPIRBanner_onDataUpdate`}. | ||
- The messages available for the platform | ||
- returns {@link "NewTab Messages".FreemiumPIRBannerData} | ||
|
||
## Notifications: | ||
- {@link "NewTab Messages".FreemiumPIRBannerActionNotification `freemiumPIRBanner_action`} | ||
- Sent when the user clicks the action button | ||
- sends {@link "NewTab Messages".FreemiumPIRBannerAction} | ||
- example payload: | ||
```json | ||
{ | ||
"id": "onboarding" | ||
} | ||
``` | ||
- {@link "NewTab Messages".FreemiumPIRBannerDismissNotification `freemiumPIRBanner_dismiss`} | ||
- Sent when the user clicks the dismiss button | ||
- sends {@link "NewTab Messages".FreemiumPIRBannerDismissAction} | ||
- example payload: | ||
```json | ||
{ | ||
"id": "scan_results" | ||
} | ||
``` | ||
|
||
## Examples: | ||
|
||
The following examples show the data types in JSON format: | ||
[messages/new-tab/examples/stats.js](../../messages/examples/freemiumPIRBanner.js) |
61 changes: 61 additions & 0 deletions
61
special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.service.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/** | ||
* @typedef {import("../../types/new-tab.js").FreemiumPIRBannerData} FreemiumPIRBannerData | ||
*/ | ||
import { Service } from '../service.js'; | ||
|
||
export class FreemiumPIRBannerService { | ||
/** | ||
* @param {import("../../src/index.js").NewTabPage} ntp - The internal data feed, expected to have a `subscribe` method. | ||
* @internal | ||
*/ | ||
constructor(ntp) { | ||
this.ntp = ntp; | ||
/** @type {Service<FreemiumPIRBannerData>} */ | ||
this.dataService = new Service({ | ||
initial: () => ntp.messaging.request('freemiumPIRBanner_getData'), | ||
subscribe: (cb) => ntp.messaging.subscribe('freemiumPIRBanner_onDataUpdate', cb), | ||
}); | ||
} | ||
|
||
name() { | ||
return 'FreemiumPIRBannerService'; | ||
} | ||
|
||
/** | ||
* @returns {Promise<FreemiumPIRBannerData>} | ||
* @internal | ||
*/ | ||
async getInitial() { | ||
return await this.dataService.fetchInitial(); | ||
} | ||
|
||
/** | ||
* @internal | ||
*/ | ||
destroy() { | ||
this.dataService.destroy(); | ||
} | ||
|
||
/** | ||
* @param {(evt: {data: FreemiumPIRBannerData, source: 'manual' | 'subscription'}) => void} cb | ||
* @internal | ||
*/ | ||
onData(cb) { | ||
return this.dataService.onData(cb); | ||
} | ||
|
||
/** | ||
* @param {string} id | ||
* @internal | ||
*/ | ||
dismiss(id) { | ||
return this.ntp.messaging.notify('freemiumPIRBanner_dismiss', { id }); | ||
} | ||
|
||
/** | ||
* @param {string} id | ||
*/ | ||
action(id) { | ||
this.ntp.messaging.notify('freemiumPIRBanner_action', { id }); | ||
} | ||
} |
Oops, something went wrong.