Skip to content

Commit 2cf7242

Browse files
authored
Merge pull request #23 from GrapeGreen/protocolling
Protocol handlers & launch handler example
2 parents 5dce6a1 + a6f6477 commit 2cf7242

File tree

4 files changed

+122
-29
lines changed

4 files changed

+122
-29
lines changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,21 @@ A demo showing off [Isolated Web Apps](https://github.com/WICG/isolated-web-apps
99
- [Borderless display mode](https://github.com/WICG/manifest-incubations/blob/gh-pages/borderless-explainer.md)
1010
- [Multiscreen capture with auto-permission](https://github.com/screen-share/capture-all-screens)
1111

12+
### Unrestricted [Protocol Handlers](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest/Reference/protocol_handlers) & [Launch Handler](https://developer.mozilla.org/en-US/docs/Web/API/Launch_Handler_API)
13+
14+
Kitchen Sink IWA is configured with `navigate-existing` launch handler
15+
and is capable of handling the `cf://` protocol (see the manifest for more
16+
details). As such, clicking on any of the links below will select an existing
17+
app instance (or create a new one if there's none running) and navigate it to
18+
`/cf.html` with some custom query params. Note that this requires the app to be
19+
installed and at least Chrome 142.
20+
- [Click me](cf://?text=Lucky&color=peachpuff) if you're feeling lucky
21+
- [Click me](cf://?text=Unlucky&color=slategrey) if you're feeling unlucky
22+
1223
## Installing as a Demo
1324

1425
If you want to try installing this through the Admin panel, use the following information:
1526

1627
- **Bundle ID** - `aiv4bxauvcu3zvbu6r5yynoh4atkzqqaoeof5mwz54b4zfywcrjuoaacai`
1728
- **Update URL** - `https://github.com/chromeos/iwa-sink/releases/latest/download/update.json`
1829

19-
## Companion extension
20-
You can find a basic companion Chrome extension in the `extension` folder. The extension uses the [chrome.identity API](https://developer.chrome.com/docs/extensions/reference/api/identity).
21-
- **Extension ID** - `gopkidjpdjfamphfffkhpmmmbmknflmk`
22-
- **Update manifest for the self-hosted extension** - [https://github.com/chromeos/iwa-sink/releases/latest/download/extension_manifest.xml](https://github.com/chromeos/iwa-sink/releases/latest/download/extension_manifest.xml)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"typescript": "^5.2.2",
1818
"vite": "^5.0.13",
1919
"vite-plugin-html-inject": "^1.1.2",
20-
"wbn-sign": "^0.2.0"
20+
"wbn-sign": "^0.2.0",
21+
"tinycolor2": "^1.6.0"
2122
}
2223
}

public/.well-known/manifest.webmanifest

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,18 @@
2323
"controlled-frame": ["self"],
2424
"window-management": ["self"],
2525
"all-screens-capture": ["self"]
26+
},
27+
"protocol_handlers": [
28+
{
29+
"protocol": "web+cf",
30+
"url": "/cf.html?params=%s"
31+
},
32+
{
33+
"protocol": "cf",
34+
"url": "/cf.html?params=%s"
35+
}
36+
],
37+
"launch_handler": {
38+
"client_mode": "navigate-existing"
2639
}
2740
}

src/cf.ts

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,100 @@
1414
* limitations under the License.
1515
*/
1616

17-
const color = document.getElementById('color') as HTMLInputElement;
18-
const lucky = document.getElementById('lucky') as HTMLInputElement;
17+
import tinycolor from 'tinycolor2';
18+
19+
const colorInput = document.getElementById('color') as HTMLInputElement;
20+
const luckyInput = document.getElementById('lucky') as HTMLInputElement;
1921
const cf = document.getElementById('cf') as ControlledFrame;
20-
const controls = document.getElementById('controls') as HTMLFormElement;
21-
22-
controls.addEventListener('submit', (e) => {
23-
e.preventDefault();
24-
});
25-
26-
color.addEventListener('change', () => {
27-
const c = color.value;
28-
const css = `body { background: ${c} !important; }`;
29-
console.log(css);
30-
cf.insertCSS({
31-
code: css,
32-
});
33-
});
34-
35-
lucky.addEventListener('change', () => {
36-
cf.executeScript({
37-
code: `
38-
document.querySelector(\`[aria-label="I'm Feeling Lucky"][role="button"]\`).value = "I'm Feeling ${lucky.value}";
39-
`,
40-
});
41-
});
22+
const controlsForm = document.getElementById('controls') as HTMLFormElement;
23+
24+
/**
25+
* Updates the background color of the controlled frame.
26+
* @param color The CSS color value.
27+
*/
28+
function updateControlledFrameBackgroundColor(color: string) {
29+
const css = `body { background: ${color} !important; }`;
30+
console.log(`Updating color to: ${color}`);
31+
cf.insertCSS({ code: css });
32+
}
33+
34+
/**
35+
* Updates the text of the "lucky" button inside the controlled frame.
36+
* @param text The new text to display.
37+
*/
38+
function updateControlledFrameLuckyButtonText(text: string) {
39+
console.log(`Updating text to: "I'm Feeling ${text}"`);
40+
// Selects both English and Dutch buttons in a single query.
41+
// The selector string uses backticks to avoid issues with single and double quotes.
42+
const script = `
43+
document.querySelectorAll(\`[aria-label="I'm Feeling Lucky"], [aria-label="Ik doe een gok"]\`).forEach(button => {
44+
button.value = "I'm Feeling ${text}";
45+
});
46+
`;
47+
cf.executeScript({ code: script });
48+
}
49+
50+
/**
51+
* Handles incoming launch parameters from the launch queue.
52+
* @param launchParams The launch parameters provided by the system.
53+
*/
54+
function handleLaunch(launchParams: LaunchParams) {
55+
if (!launchParams.targetURL) {
56+
return;
57+
}
58+
console.log(`Received launch with targetURL: ${launchParams.targetURL}`);
59+
60+
// The targetURL contains a 'params' query parameter, which is itself a URL.
61+
// We need to parse this nested URL to get the actual values.
62+
const outerParams = new URL(launchParams.targetURL).searchParams;
63+
const innerUrlString = outerParams.get('params');
64+
65+
if (!innerUrlString) {
66+
console.warn("No 'params' found in launch URL.");
67+
return;
68+
}
69+
70+
try {
71+
const innerParams = new URL(innerUrlString).searchParams;
72+
const textParam = innerParams.get('text');
73+
const colorParam = innerParams.get('color');
74+
75+
let parsedColor = null;
76+
if (colorParam) {
77+
const realColor = tinycolor(colorParam);
78+
if (!realColor.isValid()) {
79+
throw new Error(`${colorParam} does not name a valid color`);
80+
}
81+
parsedColor = realColor.toHexString();
82+
colorInput.value = parsedColor;
83+
}
84+
85+
if (textParam) {
86+
luckyInput.value = textParam;
87+
}
88+
89+
// Wait for the controlled frame to finish loading before applying changes.
90+
cf.onloadstop = () => {
91+
if (textParam) {
92+
updateControlledFrameLuckyButtonText(textParam);
93+
}
94+
if (parsedColor) {
95+
updateControlledFrameBackgroundColor(parsedColor);
96+
}
97+
};
98+
} catch (error) {
99+
console.error("Failed to parse inner URL from 'params':", error);
100+
}
101+
}
102+
103+
controlsForm.addEventListener('submit', (e) => e.preventDefault());
104+
105+
colorInput.addEventListener('change', () =>
106+
updateControlledFrameBackgroundColor(colorInput.value),
107+
);
108+
luckyInput.addEventListener('change', () =>
109+
updateControlledFrameLuckyButtonText(luckyInput.value),
110+
);
111+
112+
// Set up the launch queue consumer to handle protocol-launched events.
113+
window.launchQueue.setConsumer(handleLaunch);

0 commit comments

Comments
 (0)