Skip to content

Commit bba8ed2

Browse files
authored
Enables Duck Player custom error on all platforms (#1535)
This is a breaking change for Windows and Android.
1 parent dc1fde3 commit bba8ed2

File tree

7 files changed

+174
-29
lines changed

7 files changed

+174
-29
lines changed

special-pages/pages/duckplayer/app/features/error-detection.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { YOUTUBE_ERROR_EVENT, YOUTUBE_ERRORS } from '../providers/YouTubeErrorPr
33
/**
44
* @typedef {import("./iframe").IframeFeature} IframeFeature
55
* @typedef {import('../../types/duckplayer').YouTubeError} YouTubeError
6-
* @typedef {import('../../types/duckplayer').DuckPlayerPageSettings['customError']} CustomErrorOptions
6+
* @typedef {import('../../types/duckplayer').CustomErrorSettings} CustomErrorSettings
77
*/
88

99
/**
@@ -15,11 +15,11 @@ export class ErrorDetection {
1515
/** @type {HTMLIFrameElement} */
1616
iframe;
1717

18-
/** @type {CustomErrorOptions} */
18+
/** @type {CustomErrorSettings} */
1919
options;
2020

2121
/**
22-
* @param {CustomErrorOptions} options
22+
* @param {CustomErrorSettings} options
2323
*/
2424
constructor(options) {
2525
this.options = options;
@@ -31,8 +31,8 @@ export class ErrorDetection {
3131
iframeDidLoad(iframe) {
3232
this.iframe = iframe;
3333

34-
if (!this.options || !this.options.signInRequiredSelector) {
35-
console.log('Missing Custom Error options');
34+
if (this.options?.state !== 'enabled') {
35+
console.log('Error detection disabled');
3636
return null;
3737
}
3838

@@ -115,7 +115,8 @@ export class ErrorDetection {
115115

116116
// 2. Check for sign-in support link
117117
try {
118-
if (this.options?.signInRequiredSelector && !!iframeWindow.document.querySelector(this.options.signInRequiredSelector)) {
118+
const { settings } = this.options;
119+
if (settings?.signInRequiredSelector && !!iframeWindow.document.querySelector(settings.signInRequiredSelector)) {
119120
return YOUTUBE_ERRORS.signInRequired;
120121
}
121122
} catch (e) {

special-pages/pages/duckplayer/app/providers/YouTubeErrorProvider.jsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export function YouTubeErrorProvider({ initial = null, children }) {
4141
const [error, setError] = useState(initialError);
4242

4343
const messaging = useMessaging();
44-
const platformName = usePlatformName();
4544
const setFocusMode = useSetFocusMode();
4645

4746
// listen for updates
@@ -52,9 +51,7 @@ export function YouTubeErrorProvider({ initial = null, children }) {
5251
if (YOUTUBE_ERROR_IDS.includes(eventError) || eventError === null) {
5352
if (eventError && eventError !== error) {
5453
setFocusMode('paused');
55-
if (platformName === 'macos' || platformName === 'ios') {
56-
messaging.reportYouTubeError({ error: eventError });
57-
}
54+
messaging.reportYouTubeError({ error: eventError });
5855
} else {
5956
setFocusMode('enabled');
6057
}

special-pages/pages/duckplayer/app/settings.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,56 @@ export class Settings {
77
* @param {{state: 'enabled' | 'disabled'}} [params.pip]
88
* @param {{state: 'enabled' | 'disabled'}} [params.autoplay]
99
* @param {{state: 'enabled' | 'disabled'}} [params.focusMode]
10-
* @param {import("../types/duckplayer.js").InitialSetupResponse['settings']['customError']} [params.customError]
10+
* @param {import("../types/duckplayer.js").DuckPlayerPageSettings['customError']} [params.customError]
1111
*/
1212
constructor({
1313
platform = { name: 'macos' },
1414
pip = { state: 'disabled' },
1515
autoplay = { state: 'enabled' },
1616
focusMode = { state: 'enabled' },
17-
customError = { state: 'disabled', signInRequiredSelector: '' },
17+
customError = { state: 'disabled', settings: {}, signInRequiredSelector: '' },
1818
}) {
1919
this.platform = platform;
2020
this.pip = pip;
2121
this.autoplay = autoplay;
2222
this.focusMode = focusMode;
23-
this.customError = customError;
23+
this.customError = this.parseLegacyCustomError(customError);
24+
}
25+
26+
/**
27+
* Parses custom error settings so that both old and new schemas are accepted.
28+
*
29+
* Old schema:
30+
* {
31+
* state: "enabled",
32+
* signInRequiredSelector: "div"
33+
* }
34+
*
35+
* New schema:
36+
* {
37+
* state: "disabled",
38+
* settings: {
39+
* signInRequiredSelector: "div"
40+
* }
41+
* }
42+
*
43+
* @param {import("../types/duckplayer.js").DuckPlayerPageSettings['customError']} initialSettings
44+
* @return {import("../types/duckplayer.js").CustomErrorSettings}
45+
*/
46+
parseLegacyCustomError(initialSettings) {
47+
if (initialSettings?.state !== 'enabled') {
48+
return { state: 'disabled' };
49+
}
50+
51+
const { settings, signInRequiredSelector } = initialSettings;
52+
53+
return {
54+
state: 'enabled',
55+
settings: {
56+
...settings,
57+
...(signInRequiredSelector && { signInRequiredSelector }),
58+
},
59+
};
2460
}
2561

2662
/**
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "CustomErrorSettings",
4+
"type": "object",
5+
"description": "Configures a custom error message for YouTube errors",
6+
"required": ["state"],
7+
"properties": {
8+
"state": {
9+
"type": "string",
10+
"enum": ["enabled", "disabled"]
11+
},
12+
"settings": {
13+
"type": "object",
14+
"description": "Custom error settings",
15+
"properties": {
16+
"signInRequiredSelector": {
17+
"description": "A selector that, when not empty, indicates a sign-in required error",
18+
"type": "string"
19+
}
20+
}
21+
}
22+
}
23+
}

special-pages/pages/duckplayer/messages/initialSetup.response.json

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,18 @@
4040
}
4141
},
4242
"customError": {
43-
"type": "object",
44-
"description": "Configures a custom error message for YouTube errors",
45-
"required": ["state", "signInRequiredSelector"],
46-
"properties": {
47-
"state": {
48-
"type": "string",
49-
"enum": ["enabled", "disabled"]
50-
},
51-
"signInRequiredSelector": {
52-
"description": "A selector that, when not empty, indicates a sign-in required error",
53-
"type": "string"
43+
"allOf": [
44+
{ "$ref": "customErrorSettings.shared.json" },
45+
{
46+
"properties": {
47+
"signInRequiredSelector": {
48+
"type": "string",
49+
"description": "A selector that, when not empty, indicates a sign-in required error",
50+
"$comment": "This setting is duplicated at the top level until Apple and Windows are migrated to the settings object above"
51+
}
52+
}
5453
}
55-
}
54+
]
5655
}
5756
}
5857
},

special-pages/pages/duckplayer/types/duckplayer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,26 @@ export interface DuckPlayerPageSettings {
129129
focusMode?: {
130130
state: "enabled" | "disabled";
131131
};
132+
customError?: CustomErrorSettings & {
133+
/**
134+
* A selector that, when not empty, indicates a sign-in required error
135+
*/
136+
signInRequiredSelector?: string;
137+
};
138+
}
139+
/**
140+
* Configures a custom error message for YouTube errors
141+
*/
142+
export interface CustomErrorSettings {
143+
state: "enabled" | "disabled";
132144
/**
133-
* Configures a custom error message for YouTube errors
145+
* Custom error settings
134146
*/
135-
customError?: {
136-
state: "enabled" | "disabled";
147+
settings?: {
137148
/**
138149
* A selector that, when not empty, indicates a sign-in required error
139150
*/
140-
signInRequiredSelector: string;
151+
signInRequiredSelector?: string;
141152
};
142153
}
143154
/**
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it } from 'node:test';
2+
import { deepEqual } from 'node:assert/strict';
3+
import { Settings } from '../app/settings.js';
4+
5+
describe('parses settings', () => {
6+
it('handles disabled custom error schema', () => {
7+
const settings = new Settings({
8+
customError: {
9+
state: 'disabled',
10+
},
11+
});
12+
const expected = {
13+
state: 'disabled',
14+
};
15+
16+
deepEqual(settings.customError, expected);
17+
});
18+
it('handles old custom error schema', () => {
19+
const settings = new Settings({
20+
customError: {
21+
state: 'enabled',
22+
signInRequiredSelector: 'div',
23+
},
24+
});
25+
const expected = {
26+
state: 'enabled',
27+
settings: {
28+
signInRequiredSelector: 'div',
29+
},
30+
};
31+
32+
deepEqual(settings.customError, expected);
33+
});
34+
it('handles new custom error schema', () => {
35+
const settings = new Settings({
36+
customError: {
37+
state: 'enabled',
38+
settings: {
39+
signInRequiredSelector: 'div',
40+
},
41+
},
42+
});
43+
const expected = {
44+
state: 'enabled',
45+
settings: {
46+
signInRequiredSelector: 'div',
47+
},
48+
};
49+
50+
deepEqual(settings.customError, expected);
51+
});
52+
it('handles custom error enabled without settings', () => {
53+
const settings = new Settings({
54+
customError: {
55+
state: 'enabled',
56+
},
57+
});
58+
const expected = {
59+
state: 'enabled',
60+
settings: {},
61+
};
62+
63+
deepEqual(settings.customError, expected);
64+
});
65+
it('handles malformed custom error schema', () => {
66+
const settings = new Settings({
67+
customError: {
68+
// @ts-expect-error - Malformed object on purpose
69+
status: 'enabled',
70+
},
71+
});
72+
const expected = {
73+
state: 'disabled',
74+
};
75+
76+
deepEqual(settings.customError, expected);
77+
});
78+
});

0 commit comments

Comments
 (0)