Skip to content

chore: improve detection of iOS devices #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
9 changes: 6 additions & 3 deletions src/lib/client/adapters/webcontainer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AnsiToHtml from 'ansi-to-html';
import * as yootils from 'yootils';
import { escape_html, get_depth } from '../../../utils.js';
import { ready } from '../common/index.js';
import { isWebContainerSupported } from './utils.js';
import { is_webcontainer_supported, is_ios_device } from './utils.js';

/**
* @typedef {import("../../../../routes/tutorial/[slug]/state.js").CompilerWarning} CompilerWarning
Expand All @@ -23,11 +23,14 @@ let vm;
* @param {import('svelte/store').Writable<{ value: number, text: string }>} progress
* @param {import('svelte/store').Writable<string[]>} logs
* @param {import('svelte/store').Writable<Record<string, CompilerWarning[]>>} warnings
* @param {boolean} force
* @returns {Promise<import('$lib/types').Adapter>}
*/
export async function create(base, error, progress, logs, warnings) {
if (!isWebContainerSupported()) {
export async function create(base, error, progress, logs, warnings, force) {
if (!is_webcontainer_supported()) {
throw new Error('WebContainers are not supported by Safari 16.3 or earlier');
} else if (is_ios_device() && !force) {
throw new Error('On iOS devices, your browser may run out of memory');
}

progress.set({ value: 0, text: 'loading files' });
Expand Down
10 changes: 9 additions & 1 deletion src/lib/client/adapters/webcontainer/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Checks if WebContainer is supported on the current browser.
* This function is borrowed from [stackblitz/webcontainer-docs](https://github.com/stackblitz/webcontainer-docs/blob/369dd58b2749b085ed7642f22108a9bcbcd68fc4/docs/.vitepress/theme/components/Examples/WCEmbed/utils.ts#L4-L29)
*/
export function isWebContainerSupported() {
export function is_webcontainer_supported() {
const hasSharedArrayBuffer = 'SharedArrayBuffer' in window;
const looksLikeChrome = navigator.userAgent.toLowerCase().includes('chrome');
const looksLikeFirefox = navigator.userAgent.includes('Firefox');
Expand All @@ -28,3 +28,11 @@ export function isWebContainerSupported() {
return false;
}
}

export function is_ios_device() {
return (
/iPad|iPhone/.test(window.navigator.userAgent) ||
// on iPadOS 13 or later, UserAgent is the same as Safari on MacOS, so maxTouchPoints is used to detect it
(window.navigator.userAgent.includes('Macintosh') && window.navigator.maxTouchPoints > 1)
);
}
23 changes: 21 additions & 2 deletions src/routes/tutorial/[slug]/Loading.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script>
import { isWebContainerSupported } from "$lib/client/adapters/webcontainer/utils.js";
import { createEventDispatcher } from 'svelte';
import {
is_webcontainer_supported,
is_ios_device
} from '$lib/client/adapters/webcontainer/utils.js';

/** @type {boolean} */
export let initial;
Expand All @@ -12,12 +16,20 @@

/** @type {string} */
export let status;

/** @type {boolean} */
export let forced_booted;

const dispatch = createEventDispatcher();
</script>

<div class="loading" class:error>
{#if error}
{#if !isWebContainerSupported()}
{#if !is_webcontainer_supported()}
<p>This app requires modern web platform features. Please use a browser other than Safari.</p>
{:else if is_ios_device() && !forced_booted}
<p>On iOS devices, your browser may run out of memory.</p>
<button on:click={() => dispatch('force_boot')}>Run the tutorial</button>
{:else}
<small>{error.message}</small>
<h2>Yikes!</h2>
Expand Down Expand Up @@ -145,6 +157,13 @@
height: 10rem;
}

button {
background: var(--sk-theme-1);
color: white;
padding: 0.5rem;
height: 4rem;
}

@media (prefers-color-scheme: dark) {
.loading {
--faded: #444;
Expand Down
21 changes: 19 additions & 2 deletions src/routes/tutorial/[slug]/Output.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import { onMount } from 'svelte';
import Chrome from './Chrome.svelte';
import Loading from './Loading.svelte';
import { base, error, logs, progress, subscribe } from './adapter';
import { files } from './state.js';
import { base, create_adapter, error, logs, progress, reset, subscribe } from './adapter.js';

/** @type {import('$lib/types').Exercise} */
export let exercise;
Expand Down Expand Up @@ -109,6 +110,15 @@
iframe.style.visibility = 'visible';
}
}

let forced_booted = false;

function force_boot_adapter() {
$error = null;
forced_booted = true;
create_adapter(true);
reset($files);
}
</script>

<svelte:window on:message={handle_message} />
Expand Down Expand Up @@ -137,7 +147,14 @@
{/if}

{#if paused || loading || $error}
<Loading {initial} error={$error} progress={$progress.value} status={$progress.text} />
<Loading
{initial}
error={$error}
progress={$progress.value}
status={$progress.text}
{forced_booted}
on:force_boot={() => force_boot_adapter()}
/>
{/if}

<div class="terminal" class:visible={terminal_visible}>
Expand Down
8 changes: 6 additions & 2 deletions src/routes/tutorial/[slug]/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export const warnings = writable({});
/** @type {Promise<import('$lib/types').Adapter>} */
let ready = new Promise(() => {});

if (browser) {
export function create_adapter(force = false) {
ready = new Promise(async (fulfil, reject) => {
try {
const module = await import('$lib/client/adapters/webcontainer/index.js');
const adapter = await module.create(base, error, progress, logs, warnings);
const adapter = await module.create(base, error, progress, logs, warnings, force);

fulfil(adapter);
} catch (error) {
Expand All @@ -34,6 +34,10 @@ if (browser) {
});
}

if (browser) {
create_adapter();
}

/** @typedef {'reload'} EventName */

/** @type {Map<EventName, Set<() => void>>} */
Expand Down