Skip to content

Commit

Permalink
add app preview notice
Browse files Browse the repository at this point in the history
  • Loading branch information
a-type committed Mar 26, 2024
1 parent 36ad252 commit bb4fad7
Show file tree
Hide file tree
Showing 13 changed files with 324 additions and 352 deletions.
3 changes: 2 additions & 1 deletion apps/gnocchi/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TooltipProvider } from '@a-type/ui/components/tooltip';
import { P, H1 } from '@a-type/ui/components/typography';
import { ParticleLayer } from '@a-type/ui/components/particles';
import { GlobalSyncingIndicator } from '@/components/sync/GlobalSyncingIndicator.jsx';
import { Provider } from '@biscuits/client';
import { AppPreviewNotice, Provider } from '@biscuits/client';
import { graphqlClient } from './graphql.js';

export function App() {
Expand All @@ -37,6 +37,7 @@ export function App() {
<Provider graphqlClient={graphqlClient} appId="gnocchi">
<GroceriesProvider>
<ParticleLayer>
<AppPreviewNotice />
<Pages />
<Toaster
position="bottom-center"
Expand Down
18 changes: 8 additions & 10 deletions apps/gnocchi/web/src/pages/Pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,21 +170,19 @@ const routes = makeRoutes([
]);

function LayoutWithNavBar() {
const big = useMediaQuery('(min-width: 640px)');
if (big) {
return (
<PageRoot>
<Outlet />
<NavBar />
</PageRoot>
);
}
return (
<PageRoot>
<SwipeOutlet scroll className="[grid-area:content]" />
<Outlet />
<NavBar />
</PageRoot>
);

// return (
// <PageRoot>
// <SwipeOutlet scroll className="[grid-area:content]" />
// <NavBar />
// </PageRoot>
// );
}

export function Pages() {
Expand Down
3 changes: 2 additions & 1 deletion apps/gnocchi/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default defineConfig(({ command, mode }) => ({
srcDir: 'src',
filename: 'service-worker.ts',
manifest: {
id: 'gnocchi-main',
name: 'Gnocchi',
short_name: 'Gnocchi',
description: 'Your grocery list, done better.',
Expand Down Expand Up @@ -70,7 +71,7 @@ export default defineConfig(({ command, mode }) => ({
],
categories: ['food'],
display: 'standalone',
start_url: '/',
start_url: '/?directLaunch=true',
share_target: {
action: 'share',
method: 'POST',
Expand Down
8 changes: 7 additions & 1 deletion apps/trip-tick/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Pages } from '@/pages/Pages.jsx';
import { clientDescriptor, hooks } from '@/store.js';
import { IconSpritesheet } from '@a-type/ui/components/icon';
import { useIsLoggedIn, Provider, createGraphQLClient } from '@biscuits/client';
import {
useIsLoggedIn,
Provider,
createGraphQLClient,
AppPreviewNotice,
} from '@biscuits/client';
import { ReactNode, Suspense } from 'react';
import { Toaster } from 'react-hot-toast';

Expand All @@ -12,6 +17,7 @@ export function App() {
<Suspense>
<Provider appId="trip-tick" graphqlClient={graphqlClient}>
<LofiProvider>
<AppPreviewNotice />
<Pages />
<IconSpritesheet />
<Toaster
Expand Down
9 changes: 5 additions & 4 deletions apps/trip-tick/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export default defineConfig({
srcDir: 'src',
filename: 'service-worker.ts',
manifest: {
name: 'packing-list',
short_name: 'packing-list',
description: '',
id: 'trip-tick-main',
name: 'Trip Tick',
short_name: 'Trip Tick',
description: 'Stress-free trip packing',
theme_color: '#ffffff',
background_color: '#ffffff',
icons: [
Expand All @@ -34,7 +35,7 @@ export default defineConfig({
],
categories: [],
display: 'standalone',
start_url: '/',
start_url: '/?directLaunch=true',
} as any,
includeAssets: ['fonts/**/*', 'images/**/*'],

Expand Down
69 changes: 0 additions & 69 deletions apps/trip-tick/web/vite.config.ts.timestamp-1710171524420.mjs

This file was deleted.

4 changes: 4 additions & 0 deletions packages/apps/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ export const appIds = apps.map((app) => app.id) as AppId[];
export function isValidAppId(appId: string): appId is AppId {
return appIds.includes(appId as AppId);
}

export const appsById = Object.fromEntries(
apps.map((app) => [app.id, app]),
) as Record<AppId, (typeof apps)[number]>;
16 changes: 12 additions & 4 deletions packages/client/src/components/AppPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
PopoverContent,
PopoverTrigger,
} from '@a-type/ui/components/popover';
import { CONFIG, useAppId } from '../index.js';
import { CONFIG, getIsPWAInstalled, useAppId } from '../index.js';
import { ReactNode, useEffect } from 'react';
import { apps } from '@biscuits/apps';
import {
Expand All @@ -31,9 +31,17 @@ export function AppPicker({ className, children }: AppPickerProps) {
const appId = payload.appId;
const app = apps.find((app) => app.id === appId);
if (app) {
window.location.href = import.meta.env.DEV
? app.devOriginOverride
: app.url;
const url = new URL(
import.meta.env.DEV ? app.devOriginOverride : app.url,
);
if (getIsPWAInstalled()) {
// when opening another app from a PWA, unless the other app's PWA
// is installed, it opens inside a frame in the current PWA. That's not
// ideal, so we add a query param to the URL to indicate that the other
// app should show a banner to the user about how to install.
url.searchParams.set('appPickerFrom', hostApp);
}
window.location.href = url.toString();
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions packages/client/src/components/AppPreviewNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useEffect } from 'react';
import { getIsMobile, getIsPWAInstalled } from '../platform.js';
import { appsById, isValidAppId } from '@biscuits/apps';
import { Button } from '@a-type/ui/components/button';
import { InstallButton } from './InstallButton.js';
import { useSnapshot } from 'valtio';
import { installState } from '../install.js';

export interface AppPreviewNoticeProps {}

export function AppPreviewNotice({}: AppPreviewNoticeProps) {
const searchParams = new URLSearchParams(window.location.search);
const appPickerFrom = searchParams.get('appPickerFrom');

const { installReady } = useSnapshot(installState);

useEffect(() => {
// consume the param if present
if (appPickerFrom) {
const searchParams = new URLSearchParams(window.location.search);
searchParams.delete('appPickerFrom');
const url = new URL(window.location.href);
url.search = searchParams.toString();
window.history.replaceState(null, '', url.toString());
}
}, [appPickerFrom]);

if (!appPickerFrom) return null;

// if we can't install this as a PWA, there's not much point in showing this warning
if (!installReady) {
return null;
}

if (!isValidAppId(appPickerFrom)) return null;
const fromApp = appsById[appPickerFrom];

// this app is probably rendered inside a frame in the other app
return (
<div className="w-full bg-accent-light p-2 flex flex-row gap-3 items-center">
<p className="text-sm text-accent-dark flex-1">
{`You're previewing this app.`}
</p>
<Button asChild>
<a href={fromApp.url} target="_blank" rel="noopener noreferrer">
{`Back to ${fromApp.name}`}
</a>
</Button>
<InstallButton />
</div>
);
}
1 change: 1 addition & 0 deletions packages/client/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './SubscriptionExpiredDialog.js';
export * from './AppPicker.js';
export * from './UserMenu.js';
export * from './PresencePeople.js';
export * from './AppPreviewNotice.js';
69 changes: 39 additions & 30 deletions packages/client/src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,56 @@
export async function requestPersistentStorage() {
if (getIsPWAInstalled() && navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log('Persistent storage:', result ? 'granted' : 'denied');
}
if (getIsPWAInstalled() && navigator.storage && navigator.storage.persist) {
const result = await navigator.storage.persist();
console.log('Persistent storage:', result ? 'granted' : 'denied');
}
}

export function getIsPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches;
return window.matchMedia('(display-mode: standalone)').matches;
}

export function getOS() {
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'];
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];

if (macosPlatforms.indexOf(platform) !== -1) {
return 'Mac OS';
} else if (iosPlatforms.indexOf(platform) !== -1) {
return 'iOS';
} else if (windowsPlatforms.indexOf(platform) !== -1) {
return 'Windows';
} else if (/Android/.test(userAgent)) {
return 'Android';
} else if (!platform && /Linux/.test(userAgent)) {
return 'Linux';
}

return 'Other';
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'];
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'];
const iosPlatforms = ['iPhone', 'iPad', 'iPod'];

if (macosPlatforms.indexOf(platform) !== -1) {
return 'Mac OS';
} else if (iosPlatforms.indexOf(platform) !== -1) {
return 'iOS';
} else if (windowsPlatforms.indexOf(platform) !== -1) {
return 'Windows';
} else if (/Android/.test(userAgent)) {
return 'Android';
} else if (!platform && /Linux/.test(userAgent)) {
return 'Linux';
}

return 'Other';
}

export function getIsSafari() {
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/WebKit/i) && !ua.match(/CriOS/i);
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/WebKit/i) && !ua.match(/CriOS/i);
}

export function getIsFirefox() {
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/Firefox/i);
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/Firefox/i);
}

export function getIsEdge() {
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/Edge/i);
const ua = navigator.userAgent.toLowerCase();
return !!ua.match(/Edge/i);
}

export function getIsMobile() {
return (
/Mobi/.test(navigator.userAgent) ||
/Android/i.test(navigator.userAgent) ||
/iPhone/i.test(navigator.userAgent) ||
/iPad/i.test(navigator.userAgent)
);
}
Loading

0 comments on commit bb4fad7

Please sign in to comment.