Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
"ignoreFiles": ["build/**/*.css", "Sources/**/*.css", "docs/**/*.css", "special-pages/pages/**/*/dist/*.css"],
"rules": {
"csstree/validator": {
"ignoreProperties": ["text-wrap", "view-transition-name"]
"ignoreProperties": ["text-wrap", "view-transition-name", "composes"]
},
"property-no-unknown": [
true,
{
"ignoreProperties": ["composes"]
}
],
"alpha-value-notation": null,
"at-rule-empty-line-before": null,
"color-function-notation": null,
Expand Down
2 changes: 1 addition & 1 deletion injected/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"test-int": "playwright test --grep-invert '@screenshots'",
"test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int",
"test-int-snapshots": "playwright test --grep '@screenshots'",
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed",
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed --pass-with-no-tests",
"test": "npm run test-unit && npm run test-int && npm run playwright",
"serve": "http-server -c-1 --port 3220 integration-test/test-pages",
"playwright": "playwright test --grep-invert '@screenshots'",
Expand Down
2 changes: 1 addition & 1 deletion special-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "node index.mjs",
"build.dev": "npm run build -- --env development",
"lint-fix": "cd ../ && npm run lint-fix",
"test-unit": "node --test \"unit-test/*\" \"pages/history/unit-tests/*\" \"pages/duckplayer/unit-tests/*\" \"pages/new-tab/app/freemium-pir-banner/unit-tests/*\" \"pages/new-tab/app/omnibar/unit-tests/*\"",
"test-unit": "node --test \"unit-test/*\" \"pages/history/unit-tests/*\" \"pages/duckplayer/unit-tests/*\" \"pages/new-tab/app/omnibar/unit-tests/*\"",
"test-int": "playwright test --grep-invert '@screenshots'",
"test-int-x": "npm run test-int",
"test-int-snapshots": "playwright test --grep '@screenshots'",
Expand Down
2 changes: 2 additions & 0 deletions special-pages/pages/new-tab/app/components/Examples.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { otherRMFExamples, RMFExamples } from '../remote-messaging-framework/com
import { updateNotificationExamples } from '../update-notification/components/UpdateNotification.examples.js';
import { activityExamples } from '../activity/components/Activity.examples.js';
import { protectionsHeadingExamples } from '../protections/components/ProtectionsHeading.examples.js';
import { subscriptionWinBackBannerExamples } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js';

/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */
export const mainExamples = {
Expand All @@ -15,6 +16,7 @@ export const mainExamples = {
...nextStepsExamples,
...privacyStatsExamples,
...RMFExamples,
...subscriptionWinBackBannerExamples,
};

export const otherExamples = {
Expand Down
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 { SubscriptionWinBackBannerConsumer } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.js';
import { SubscriptionWinBackBannerProvider } from '../subscription-winback-banner/SubscriptionWinBackBannerProvider.js';

export function factory() {
return (
<Centered data-entry-point="subscriptionWinBackBanner">
<SubscriptionWinBackBannerProvider>
<SubscriptionWinBackBannerConsumer />
</SubscriptionWinBackBannerProvider>
</Centered>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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';
import { convertMarkdownToHTMLForStrongTags } from '../../../../../shared/utils';

/**
* @typedef { import("../../../types/new-tab").FreemiumPIRBannerMessage} FreemiumPIRBannerMessage
Expand Down

This file was deleted.

41 changes: 41 additions & 0 deletions special-pages/pages/new-tab/app/mock-transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { updateNotificationExamples } from './update-notification/mocks/update-n
import { variants as nextSteps } from './next-steps/nextsteps.data.js';
import { customizerData, customizerMockTransport } from './customizer/mocks.js';
import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIRBanner.data.js';
import { subscriptionWinBackBannerDataExamples } from './subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js';
import { activityMockTransport } from './activity/mocks/activity.mock-transport.js';
import { protectionsMockTransport } from './protections/mocks/protections.mock-transport.js';
import { omnibarMockTransport } from './omnibar/mocks/omnibar.mock-transport.js';
Expand Down Expand Up @@ -95,6 +96,7 @@ export function mockTransport() {
const rmfSubscriptions = new Map();
const freemiumPIRBannerSubscriptions = new Map();
const nextStepsSubscriptions = new Map();
const subscriptionWinBackBannerSubscriptions = new Map();

function clearRmf() {
const listeners = rmfSubscriptions.get('rmf_onDataUpdate') || [];
Expand Down Expand Up @@ -163,6 +165,14 @@ export function mockTransport() {
console.log('ignoring freemiumPIRBanner_dismiss', msg.params);
return;
}
case 'winBackOffer_action': {
console.log('ignoring winBackOffer_action', msg.params);
return;
}
case 'winBackOffer_dismiss': {
console.log('ignoring winBackOffer_dismiss', msg.params);
return;
}
case 'favorites_setConfig': {
if (!msg.params) throw new Error('unreachable');

Expand Down Expand Up @@ -256,6 +266,24 @@ export function mockTransport() {
}
return () => {};
}
case 'winBackOffer_onDataUpdate': {
// store the callback for later (eg: dismiss)
const prev = subscriptionWinBackBannerSubscriptions.get('winBackOffer_onDataUpdate') || [];
const next = [...prev];
next.push(cb);
subscriptionWinBackBannerSubscriptions.set('winBackOffer_onDataUpdate', next);

const subscriptionWinBackBannerParam = url.searchParams.get('winback');

if (
subscriptionWinBackBannerParam !== null &&
subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples
) {
const message = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam];
cb(message);
}
return () => {};
}
case 'nextSteps_onDataUpdate': {
const prev = nextStepsSubscriptions.get('nextSteps_onDataUpdate') || [];
const next = [...prev];
Expand Down Expand Up @@ -471,6 +499,18 @@ export function mockTransport() {

return Promise.resolve(freemiumPIRBannerMessage);
}
case 'winBackOffer_getData': {
/** @type {import('../types/new-tab.ts').SubscriptionWinBackBannerData} */
let subscriptionWinBackBannerMessage = { content: null };

const subscriptionWinBackBannerParam = url.searchParams.get('winback');

if (subscriptionWinBackBannerParam && subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples) {
subscriptionWinBackBannerMessage = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam];
}

return Promise.resolve(subscriptionWinBackBannerMessage);
}
case 'favorites_getData': {
const param = url.searchParams.get('favorites');
let data;
Expand Down Expand Up @@ -514,6 +554,7 @@ export function initialSetup(url) {
{ id: 'updateNotification' },
{ id: 'rmf' },
{ id: 'freemiumPIRBanner' },
{ id: 'subscriptionWinBackBanner' },
{ id: 'nextSteps' },
{ id: 'favorites' },
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { createContext, h } from 'preact';
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks';
import { useMessaging } from '../types.js';
import { SubscriptionWinBackBannerService } from './subscriptionWinBackBanner.service.js';
import { reducer, useDataSubscription, useInitialData } from '../service.hooks.js';

/**
* @typedef {import('../../types/new-tab.js').SubscriptionWinBackBannerData} SubscriptionWinBackBannerData
* @typedef {import('../service.hooks.js').State<SubscriptionWinBackBannerData, undefined>} State
* @typedef {import('../service.hooks.js').Events<SubscriptionWinBackBannerData, undefined>} Events
*/

/**
* These are the values exposed to consumers.
*/
export const SubscriptionWinBackBannerContext = 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 SubscriptionWinBackBannerDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch<Events>} */ ({}));

/**
* A data provider that will use `SubscriptionWinBackBannerService` to fetch data, subscribe
* to updates and modify state.
*
* @param {Object} props
* @param {import("preact").ComponentChild} props.children
*/
export function SubscriptionWinBackBannerProvider(props) {
const initial = /** @type {State} */ ({
status: 'idle',
data: null,
config: null,
});

// const [state, dispatch] = useReducer(withLog('SubscriptionWinBackBannerProvider', reducer), initial)
const [state, dispatch] = useReducer(reducer, initial);

// create an instance of `SubscriptionWinBackBannerService` 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 (
<SubscriptionWinBackBannerContext.Provider value={{ state, dismiss, action }}>
<SubscriptionWinBackBannerDispatchContext.Provider value={dispatch}>
{props.children}
</SubscriptionWinBackBannerDispatchContext.Provider>
</SubscriptionWinBackBannerContext.Provider>
);
}

/**
* @return {import("preact").RefObject<SubscriptionWinBackBannerService>}
*/
export function useService() {
const service = useRef(/** @type {SubscriptionWinBackBannerService|null} */ (null));
const ntp = useMessaging();
useEffect(() => {
const stats = new SubscriptionWinBackBannerService(ntp);
service.current = stats;
return () => {
stats.destroy();
};
}, [ntp]);
return service;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { h } from 'preact';
import { noop } from '../../utils.js';
import { SubscriptionWinBackBanner } from './SubscriptionWinBackBanner.js';
import { subscriptionWinBackBannerDataExamples } from '../mocks/subscriptionWinBackBanner.data.js';

/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */

export const subscriptionWinBackBannerExamples = {
'subscriptionWinBackBanner.winback_last_day': {
factory: () => (
<SubscriptionWinBackBanner
message={subscriptionWinBackBannerDataExamples.winback_last_day.content}
dismiss={noop('winBackOffer_dismiss')}
action={noop('winBackOffer_action')}
/>
),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import cn from 'classnames';
import { h } from 'preact';
import { Button } from '../../../../../shared/components/Button/Button';
import { DismissButton } from '../../components/DismissButton';
import styles from './SubscriptionWinBackBanner.module.css';
import { SubscriptionWinBackBannerContext } from '../SubscriptionWinBackBannerProvider';
import { useContext } from 'preact/hooks';
import { convertMarkdownToHTMLForStrongTags } from '../../../../../shared/utils';

/**
* @typedef { import("../../../types/new-tab").SubscriptionWinBackBannerMessage} SubscriptionWinBackBannerMessage
* @param {object} props
* @param {SubscriptionWinBackBannerMessage} props.message
* @param {(id: string) => void} props.dismiss
* @param {(id: string) => void} props.action
*/
export function SubscriptionWinBackBanner({ message, action, dismiss }) {
const processedMessageDescription = convertMarkdownToHTMLForStrongTags(message.descriptionText);
return (
<div id={message.id} class={cn(styles.root, styles.icon)}>
<span class={styles.iconBlock}>
<img aria-hidden="true" src={`./icons/Subscription-Clock-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 size="md" variant="accent" onClick={() => action(message.id)}>
{message.actionText}
</Button>
</div>
)}
{message.id && dismiss && <DismissButton className={styles.dismissBtn} onClick={() => dismiss(message.id)} />}
</div>
);
}

export function SubscriptionWinBackBannerConsumer() {
const { state, action, dismiss } = useContext(SubscriptionWinBackBannerContext);

if (state.status === 'ready' && state.data.content) {
return <SubscriptionWinBackBanner message={state.data.content} action={action} dismiss={dismiss} />;
}
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Using CSS Modules 'composes' to import styles from FreemiumPIRBanner.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very cool! today i learned. 🙌

* This avoids code duplication and keeps both components in sync.
* See: https://github.com/css-modules/css-modules/blob/master/docs/composition.md
*/

.root {
composes: root from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.iconBlock {
composes: iconBlock from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.content {
composes: content from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.title {
composes: title from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.description {
composes: description from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.btnBlock {
composes: btnBlock from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.btnRow {
composes: btnRow from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.dismissBtn {
composes: dismissBtn from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}

.icon {
composes: icon from '../../freemium-pir-banner/components/FreemiumPIRBanner.module.css';
}
Loading