diff --git a/src/components/AppBeta.tsx b/src/components/AppBeta.tsx new file mode 100644 index 0000000..9cf1337 --- /dev/null +++ b/src/components/AppBeta.tsx @@ -0,0 +1,189 @@ +import { Smartphone, X } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; +import { + ANDROID_BETA_GROUP_URL, + ANDROID_BETA_OPT_IN_URL, + IOS_BETA_URL, +} from '~/components/Site/constants'; +import { Button } from '~/components/ui/button'; +import { cn } from '~/lib/utils'; + +export type DevicePlatform = 'ios' | 'android' | 'desktop'; + +// One key per card variant, so dismissing the iPhone-only version doesn't hide the Android one later +const dismissKey = (platform: DevicePlatform) => + `splitpro:app-beta-card-dismissed:${platform}:${null === ANDROID_BETA_GROUP_URL ? 'ios' : 'all'}`; + +const detectPlatform = (): DevicePlatform => { + const ua = navigator.userAgent; + if (/android/i.test(ua)) { + return 'android'; + } + // iPadOS reports a Mac user agent, so a touch screen is what gives it away. + if (/iphone|ipad|ipod/i.test(ua) || (/macintosh/i.test(ua) && 1 < navigator.maxTouchPoints)) { + return 'ios'; + } + return 'desktop'; +}; + +/** The device platform, or null until mounted, so the server render and first client render match. */ +export const useDevicePlatform = () => { + const [platform, setPlatform] = useState(null); + + useEffect(() => { + setPlatform(detectPlatform()); + }, []); + + return platform; +}; + +const BetaLink: React.FC<{ href: string; children: React.ReactNode; className?: string }> = ({ + href, + children, + className, +}) => ( + + {children} + +); + +const IosBeta: React.FC = () => ( +
+ +

Needs Apple's free TestFlight app.

+
+); + +const AndroidBetaSteps: React.FC<{ groupUrl: string }> = ({ groupUrl }) => ( +
    +
  1. + Join the testers group +
  2. +
  3. + Tap "Become a tester" on the{' '} + opt-in page +
  4. +
+); + +/** Dismissible card on the Balances page pointing at the native app beta for this device. */ +export const AppBetaCard: React.FC<{ className?: string }> = ({ className }) => { + const platform = useDevicePlatform(); + // Hidden until we've read localStorage, so it never flashes for people who dismissed it. + const [dismissed, setDismissed] = useState(true); + + useEffect(() => { + if (null === platform) { + return; + } + try { + setDismissed('1' === localStorage.getItem(dismissKey(platform))); + } catch { + setDismissed(false); + } + }, [platform]); + + const dismiss = useCallback(() => { + setDismissed(true); + if (null === platform) { + return; + } + try { + localStorage.setItem(dismissKey(platform), '1'); + } catch { + // Private mode or storage disabled: it's hidden until the next reload, which is fine. + } + }, [platform]); + + if (null === platform || dismissed) { + return null; + } + + const androidGroupUrl = ANDROID_BETA_GROUP_URL; + + if ('android' === platform && null === androidGroupUrl) { + return null; + } + + const closeButton = ( + + ); + + if ('desktop' === platform) { + return ( +
+ {null !== androidGroupUrl ? ( +

+ The phone apps are in beta: iPhone ยท Android:{' '} + join the group, then{' '} + opt in +

+ ) : ( +

+ The iPhone app is in beta. Join on TestFlight +

+ )} + {closeButton} +
+ ); + } + + return ( +
+ +
+
+
+

+ The {'ios' === platform ? 'iPhone' : 'Android'} app is in beta +

+

Try it early and tell me what breaks.

+
+ {closeButton} +
+ {'ios' === platform ? : null} + {'android' === platform && null !== androidGroupUrl ? ( + + ) : null} +
+
+ ); +}; + +/** Beta links for the "Download App" drawer: every platform that has a beta open. */ +export const AppBetaLinks: React.FC = () => { + const androidGroupUrl = ANDROID_BETA_GROUP_URL; + + return ( +
+
+

iPhone and iPad

+ +
+ {null !== androidGroupUrl ? ( +
+

Android

+ +
+ ) : null} +
+ ); +}; diff --git a/src/components/InstallApp.tsx b/src/components/InstallApp.tsx index ba1a907..290a742 100644 --- a/src/components/InstallApp.tsx +++ b/src/components/InstallApp.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Button } from './ui/button'; import { Download } from 'lucide-react'; import { AppDrawer } from './ui/drawer'; +import { AppBetaLinks } from './AppBeta'; const InstallApp: React.FC = () => { const [isStandalone, setIsStandalone] = React.useState(false); @@ -44,29 +45,36 @@ const InstallApp: React.FC = () => { shouldCloseOnAction >
-

You can download SplitPro as a PWA to your home screen

+
+

The native app is in beta. Try it early and tell me what breaks.

+ +
-

- If you are using iOS, checkout this{' '} - - video - -

+
+

Or add SplitPro to your home screen from the browser.

-

- If you are using Android, checkout this{' '} - - Video - -

+

+ If you are using iOS, checkout this{' '} + + video + +

+ +

+ If you are using Android, checkout this{' '} + + Video + +

+
diff --git a/src/components/Site/blocks.tsx b/src/components/Site/blocks.tsx index 59dc9ad..822a527 100644 --- a/src/components/Site/blocks.tsx +++ b/src/components/Site/blocks.tsx @@ -22,9 +22,12 @@ export const PrimaryLink: React.FC<{ href: string; children: React.ReactNode; className?: string; -}> = ({ href, children, className }) => ( + /** Opens in a new tab. */ + external?: boolean; +}> = ({ href, children, className, external }) => ( = ({ href, children, className }) => ( + /** Opens in a new tab. */ + external?: boolean; +}> = ({ href, children, className, external }) => ( { function shareWithFriends() { @@ -54,6 +55,7 @@ const BalancePage: NextPageWithUser = () => { } > +
{balanceQuery.data?.youOwe.length ? ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a58ea59..0104700 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -7,11 +7,19 @@ import { Import, Repeat, SlidersHorizontal, + Smartphone, Users, } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; -import { GITHUB_URL, QUICK_SPLIT_PATH, SITE_URL } from '~/components/Site/constants'; +import { + ANDROID_BETA_GROUP_URL, + ANDROID_BETA_OPT_IN_URL, + GITHUB_URL, + IOS_BETA_URL, + QUICK_SPLIT_PATH, + SITE_URL, +} from '~/components/Site/constants'; import { CtaBand, Faq, @@ -31,8 +39,11 @@ const FAQS: FaqItem[] = [ }, { question: 'Do I need to install an app?', - answer: - 'No. SplitPro runs in your browser. On your phone you can add it to your home screen, where it opens like any other app and can send you notifications.', + answer: `No. SplitPro runs in your browser, and on your phone you can add it to your home screen. ${ + null !== ANDROID_BETA_GROUP_URL + ? 'Native iPhone and Android apps are in beta too.' + : 'A native iPhone app is in beta too, with Android coming soon.' + }`, }, { question: 'Do my friends need an account?', @@ -89,6 +100,7 @@ export default function Home() { +
@@ -224,6 +236,53 @@ const Showcase = () => (
); +const AppBeta = () => { + const androidGroupUrl = ANDROID_BETA_GROUP_URL; + + return ( +
+
+
+

+ + Beta +

+

+ {null !== androidGroupUrl + ? 'SplitPro for iPhone and Android is in beta' + : 'SplitPro for iPhone is in beta'} +

+

+ The native app is still rough around the edges. Try it early and tell me what breaks. +

+
+
+
+ + Join the iOS beta + +

Needs Apple's free TestFlight app.

+
+ {null !== androidGroupUrl ? ( +
+

Android, in two steps

+
+ + 1. Join the testers group + + + 2. Become a tester + +
+

Use the same Google account for both.

+
+ ) : null} +
+
+
+ ); +}; + const QuickSplitPromo = () => (