Skip to content

Commit 02713ca

Browse files
authored
jingram/subscription win back banner: Adds Subscription Win-Back Banner (#2014)
* Move utility functions to shared file and update unit tests Remove freemium dir from test-unit script * Messaging support for the subscription win-back banner * Create win-back banner component and supporting files Include supporting svg * Wire up mock transport * Generate types * Wire up integration test for win-back banner on NTP * Update button styles * Update new-tab readme * Add entry point for Subscription Win-back banner * Resolve Unknown property `composes` * Resolve Unexpected unknown property "composes" property-no-unknown * Fix unit test issue * Patch: Update test-int-snapshots-update in injected/package.json * Use accentBrand colors for MacOS button * Update test to pass the correct query param value
1 parent 6f24908 commit 02713ca

31 files changed

+651
-38
lines changed

.stylelintrc.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
"ignoreFiles": ["build/**/*.css", "Sources/**/*.css", "docs/**/*.css", "special-pages/pages/**/*/dist/*.css"],
55
"rules": {
66
"csstree/validator": {
7-
"ignoreProperties": ["text-wrap", "view-transition-name"]
7+
"ignoreProperties": ["text-wrap", "view-transition-name", "composes"]
88
},
9+
"property-no-unknown": [
10+
true,
11+
{
12+
"ignoreProperties": ["composes"]
13+
}
14+
],
915
"alpha-value-notation": null,
1016
"at-rule-empty-line-before": null,
1117
"color-function-notation": null,

injected/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"test-int": "playwright test --grep-invert '@screenshots'",
1616
"test-int-x": "xvfb-run --server-args='-screen 0 1024x768x24' npm run test-int",
1717
"test-int-snapshots": "playwright test --grep '@screenshots'",
18-
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed",
18+
"test-int-snapshots-update": "playwright test --grep '@screenshots' --update-snapshots --last-failed --pass-with-no-tests",
1919
"test": "npm run test-unit && npm run test-int && npm run playwright",
2020
"serve": "http-server -c-1 --port 3220 integration-test/test-pages",
2121
"playwright": "playwright test --grep-invert '@screenshots'",

special-pages/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build": "node index.mjs",
1111
"build.dev": "npm run build -- --env development",
1212
"lint-fix": "cd ../ && npm run lint-fix",
13-
"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/*\"",
13+
"test-unit": "node --test \"unit-test/*\" \"pages/history/unit-tests/*\" \"pages/duckplayer/unit-tests/*\" \"pages/new-tab/app/omnibar/unit-tests/*\"",
1414
"test-int": "playwright test --grep-invert '@screenshots'",
1515
"test-int-x": "npm run test-int",
1616
"test-int-snapshots": "playwright test --grep '@screenshots'",

special-pages/pages/new-tab/app/components/Examples.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { otherRMFExamples, RMFExamples } from '../remote-messaging-framework/com
77
import { updateNotificationExamples } from '../update-notification/components/UpdateNotification.examples.js';
88
import { activityExamples } from '../activity/components/Activity.examples.js';
99
import { protectionsHeadingExamples } from '../protections/components/ProtectionsHeading.examples.js';
10+
import { subscriptionWinBackBannerExamples } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.examples.js';
1011

1112
/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */
1213
export const mainExamples = {
@@ -15,6 +16,7 @@ export const mainExamples = {
1516
...nextStepsExamples,
1617
...privacyStatsExamples,
1718
...RMFExamples,
19+
...subscriptionWinBackBannerExamples,
1820
};
1921

2022
export const otherExamples = {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { h } from 'preact';
2+
import { Centered } from '../components/Layout.js';
3+
import { SubscriptionWinBackBannerConsumer } from '../subscription-winback-banner/components/SubscriptionWinBackBanner.js';
4+
import { SubscriptionWinBackBannerProvider } from '../subscription-winback-banner/SubscriptionWinBackBannerProvider.js';
5+
6+
export function factory() {
7+
return (
8+
<Centered data-entry-point="subscriptionWinBackBanner">
9+
<SubscriptionWinBackBannerProvider>
10+
<SubscriptionWinBackBannerConsumer />
11+
</SubscriptionWinBackBannerProvider>
12+
</Centered>
13+
);
14+
}

special-pages/pages/new-tab/app/freemium-pir-banner/components/FreemiumPIRBanner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DismissButton } from '../../components/DismissButton';
55
import styles from './FreemiumPIRBanner.module.css';
66
import { FreemiumPIRBannerContext } from '../FreemiumPIRBannerProvider';
77
import { useContext } from 'preact/hooks';
8-
import { convertMarkdownToHTMLForStrongTags } from '../freemiumPIRBanner.utils';
8+
import { convertMarkdownToHTMLForStrongTags } from '../../../../../shared/utils';
99

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

special-pages/pages/new-tab/app/freemium-pir-banner/freemiumPIRBanner.utils.js

Lines changed: 0 additions & 30 deletions
This file was deleted.

special-pages/pages/new-tab/app/mock-transport.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { updateNotificationExamples } from './update-notification/mocks/update-n
77
import { variants as nextSteps } from './next-steps/nextsteps.data.js';
88
import { customizerData, customizerMockTransport } from './customizer/mocks.js';
99
import { freemiumPIRDataExamples } from './freemium-pir-banner/mocks/freemiumPIRBanner.data.js';
10+
import { subscriptionWinBackBannerDataExamples } from './subscription-winback-banner/mocks/subscriptionWinBackBanner.data.js';
1011
import { activityMockTransport } from './activity/mocks/activity.mock-transport.js';
1112
import { protectionsMockTransport } from './protections/mocks/protections.mock-transport.js';
1213
import { omnibarMockTransport } from './omnibar/mocks/omnibar.mock-transport.js';
@@ -95,6 +96,7 @@ export function mockTransport() {
9596
const rmfSubscriptions = new Map();
9697
const freemiumPIRBannerSubscriptions = new Map();
9798
const nextStepsSubscriptions = new Map();
99+
const subscriptionWinBackBannerSubscriptions = new Map();
98100

99101
function clearRmf() {
100102
const listeners = rmfSubscriptions.get('rmf_onDataUpdate') || [];
@@ -163,6 +165,14 @@ export function mockTransport() {
163165
console.log('ignoring freemiumPIRBanner_dismiss', msg.params);
164166
return;
165167
}
168+
case 'winBackOffer_action': {
169+
console.log('ignoring winBackOffer_action', msg.params);
170+
return;
171+
}
172+
case 'winBackOffer_dismiss': {
173+
console.log('ignoring winBackOffer_dismiss', msg.params);
174+
return;
175+
}
166176
case 'favorites_setConfig': {
167177
if (!msg.params) throw new Error('unreachable');
168178

@@ -256,6 +266,24 @@ export function mockTransport() {
256266
}
257267
return () => {};
258268
}
269+
case 'winBackOffer_onDataUpdate': {
270+
// store the callback for later (eg: dismiss)
271+
const prev = subscriptionWinBackBannerSubscriptions.get('winBackOffer_onDataUpdate') || [];
272+
const next = [...prev];
273+
next.push(cb);
274+
subscriptionWinBackBannerSubscriptions.set('winBackOffer_onDataUpdate', next);
275+
276+
const subscriptionWinBackBannerParam = url.searchParams.get('winback');
277+
278+
if (
279+
subscriptionWinBackBannerParam !== null &&
280+
subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples
281+
) {
282+
const message = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam];
283+
cb(message);
284+
}
285+
return () => {};
286+
}
259287
case 'nextSteps_onDataUpdate': {
260288
const prev = nextStepsSubscriptions.get('nextSteps_onDataUpdate') || [];
261289
const next = [...prev];
@@ -471,6 +499,18 @@ export function mockTransport() {
471499

472500
return Promise.resolve(freemiumPIRBannerMessage);
473501
}
502+
case 'winBackOffer_getData': {
503+
/** @type {import('../types/new-tab.ts').SubscriptionWinBackBannerData} */
504+
let subscriptionWinBackBannerMessage = { content: null };
505+
506+
const subscriptionWinBackBannerParam = url.searchParams.get('winback');
507+
508+
if (subscriptionWinBackBannerParam && subscriptionWinBackBannerParam in subscriptionWinBackBannerDataExamples) {
509+
subscriptionWinBackBannerMessage = subscriptionWinBackBannerDataExamples[subscriptionWinBackBannerParam];
510+
}
511+
512+
return Promise.resolve(subscriptionWinBackBannerMessage);
513+
}
474514
case 'favorites_getData': {
475515
const param = url.searchParams.get('favorites');
476516
let data;
@@ -514,6 +554,7 @@ export function initialSetup(url) {
514554
{ id: 'updateNotification' },
515555
{ id: 'rmf' },
516556
{ id: 'freemiumPIRBanner' },
557+
{ id: 'subscriptionWinBackBanner' },
517558
{ id: 'nextSteps' },
518559
{ id: 'favorites' },
519560
];
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { createContext, h } from 'preact';
2+
import { useCallback, useEffect, useReducer, useRef } from 'preact/hooks';
3+
import { useMessaging } from '../types.js';
4+
import { SubscriptionWinBackBannerService } from './subscriptionWinBackBanner.service.js';
5+
import { reducer, useDataSubscription, useInitialData } from '../service.hooks.js';
6+
7+
/**
8+
* @typedef {import('../../types/new-tab.js').SubscriptionWinBackBannerData} SubscriptionWinBackBannerData
9+
* @typedef {import('../service.hooks.js').State<SubscriptionWinBackBannerData, undefined>} State
10+
* @typedef {import('../service.hooks.js').Events<SubscriptionWinBackBannerData, undefined>} Events
11+
*/
12+
13+
/**
14+
* These are the values exposed to consumers.
15+
*/
16+
export const SubscriptionWinBackBannerContext = createContext({
17+
/** @type {State} */
18+
state: { status: 'idle', data: null, config: null },
19+
/** @type {(id: string) => void} */
20+
dismiss: (id) => {
21+
throw new Error('must implement dismiss' + id);
22+
},
23+
/** @type {(id: string) => void} */
24+
action: (id) => {
25+
throw new Error('must implement action' + id);
26+
},
27+
});
28+
29+
export const SubscriptionWinBackBannerDispatchContext = createContext(/** @type {import("preact/hooks").Dispatch<Events>} */ ({}));
30+
31+
/**
32+
* A data provider that will use `SubscriptionWinBackBannerService` to fetch data, subscribe
33+
* to updates and modify state.
34+
*
35+
* @param {Object} props
36+
* @param {import("preact").ComponentChild} props.children
37+
*/
38+
export function SubscriptionWinBackBannerProvider(props) {
39+
const initial = /** @type {State} */ ({
40+
status: 'idle',
41+
data: null,
42+
config: null,
43+
});
44+
45+
// const [state, dispatch] = useReducer(withLog('SubscriptionWinBackBannerProvider', reducer), initial)
46+
const [state, dispatch] = useReducer(reducer, initial);
47+
48+
// create an instance of `SubscriptionWinBackBannerService` for the lifespan of this component.
49+
const service = useService();
50+
51+
// get initial data
52+
useInitialData({ dispatch, service });
53+
54+
// subscribe to data updates
55+
useDataSubscription({ dispatch, service });
56+
57+
// todo(valerie): implement onDismiss in the service
58+
const dismiss = useCallback(
59+
(id) => {
60+
console.log('onDismiss');
61+
service.current?.dismiss(id);
62+
},
63+
[service],
64+
);
65+
66+
const action = useCallback(
67+
(id) => {
68+
service.current?.action(id);
69+
},
70+
[service],
71+
);
72+
73+
return (
74+
<SubscriptionWinBackBannerContext.Provider value={{ state, dismiss, action }}>
75+
<SubscriptionWinBackBannerDispatchContext.Provider value={dispatch}>
76+
{props.children}
77+
</SubscriptionWinBackBannerDispatchContext.Provider>
78+
</SubscriptionWinBackBannerContext.Provider>
79+
);
80+
}
81+
82+
/**
83+
* @return {import("preact").RefObject<SubscriptionWinBackBannerService>}
84+
*/
85+
export function useService() {
86+
const service = useRef(/** @type {SubscriptionWinBackBannerService|null} */ (null));
87+
const ntp = useMessaging();
88+
useEffect(() => {
89+
const stats = new SubscriptionWinBackBannerService(ntp);
90+
service.current = stats;
91+
return () => {
92+
stats.destroy();
93+
};
94+
}, [ntp]);
95+
return service;
96+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { h } from 'preact';
2+
import { noop } from '../../utils.js';
3+
import { SubscriptionWinBackBanner } from './SubscriptionWinBackBanner.js';
4+
import { subscriptionWinBackBannerDataExamples } from '../mocks/subscriptionWinBackBanner.data.js';
5+
6+
/** @type {Record<string, {factory: () => import("preact").ComponentChild}>} */
7+
8+
export const subscriptionWinBackBannerExamples = {
9+
'subscriptionWinBackBanner.winback_last_day': {
10+
factory: () => (
11+
<SubscriptionWinBackBanner
12+
message={subscriptionWinBackBannerDataExamples.winback_last_day.content}
13+
dismiss={noop('winBackOffer_dismiss')}
14+
action={noop('winBackOffer_action')}
15+
/>
16+
),
17+
},
18+
};

0 commit comments

Comments
 (0)