Browse Source

Link the technician app on the App Store next to Google Play (#519)

Bernt Christian Egeland 1 day ago
parent
commit
1197f7acf9

+ 1 - 0
messages/de/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Noch kein Zugriff",
     "noAccessBody": "Ihr Konto gehört zu {workshop}, hat aber noch keine Rolle, deshalb gibt es hier nichts zu öffnen. Bitten Sie einen Inhaber oder Administrator Ihrer Werkstatt um eine Rolle.",
     "setupPageInstall": "Bei Google Play laden",
+    "setupPageInstallIos": "Laden im App Store",
     "invalidInviteEmail": "Prüfen Sie die E-Mail-Adresse. Sie sollte so aussehen: name@beispiel.de."
   },
   "invoice": {

+ 1 - 0
messages/en/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "No access yet",
     "noAccessBody": "Your account is part of {workshop}, but it has not been given a role, so there is nothing here you can open yet. Ask an owner or an admin at your workshop to give you one.",
     "setupPageInstall": "Get it on Google Play",
+    "setupPageInstallIos": "Download on the App Store",
     "invalidInviteEmail": "Check the email address. It should look like name@example.com."
   },
   "invoice": {

+ 1 - 0
messages/es/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Sin acceso todavía",
     "noAccessBody": "Su cuenta pertenece a {workshop}, pero no tiene un rol asignado, así que aquí no hay nada que pueda abrir. Pida un rol a un propietario o administrador de su taller.",
     "setupPageInstall": "Consíguelo en Google Play",
+    "setupPageInstallIos": "Descárgalo en el App Store",
     "invalidInviteEmail": "Revisa la dirección de correo. Debe tener la forma nombre@ejemplo.com."
   },
   "invoice": {

+ 1 - 0
messages/fr/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Pas encore d’accès",
     "noAccessBody": "Votre compte fait partie de {workshop}, mais aucun rôle ne lui a été attribué, donc il n’y a rien à ouvrir ici. Demandez un rôle à un propriétaire ou un administrateur de votre atelier.",
     "setupPageInstall": "Télécharger sur Google Play",
+    "setupPageInstallIos": "Télécharger dans l'App Store",
     "invalidInviteEmail": "Vérifiez l'adresse e-mail. Elle doit ressembler à nom@exemple.fr."
   },
   "invoice": {

+ 1 - 0
messages/it/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ancora nessun accesso",
     "noAccessBody": "Il tuo account fa parte di {workshop}, ma non ha ancora un ruolo, quindi qui non c’è nulla da aprire. Chiedi un ruolo a un titolare o a un amministratore della tua officina.",
     "setupPageInstall": "Scaricala su Google Play",
+    "setupPageInstallIos": "Scarica su App Store",
     "invalidInviteEmail": "Controlla l'indirizzo email. Deve avere la forma nome@esempio.it."
   },
   "invoice": {

+ 1 - 0
messages/lt/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Prieigos kol kas nėra",
     "noAccessBody": "Jūsų paskyra priklauso {workshop}, bet jai dar nesuteikta rolė, todėl čia nėra ką atidaryti. Paprašykite dirbtuvių savininko arba administratoriaus suteikti jums rolę.",
     "setupPageInstall": "Atsisiųsti iš „Google Play“",
+    "setupPageInstallIos": "Atsisiųsti iš „App Store“",
     "invalidInviteEmail": "Patikrinkite el. pašto adresą. Jis turėtų atrodyti taip: vardas@pavyzdys.lt."
   },
   "invoice": {

+ 1 - 0
messages/nb/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ingen tilgang ennå",
     "noAccessBody": "Kontoen din tilhører {workshop}, men den har ikke fått en rolle ennå, så det er ingenting her du kan åpne. Be en eier eller administrator på verkstedet om å gi deg en.",
     "setupPageInstall": "Last ned fra Google Play",
+    "setupPageInstallIos": "Last ned fra App Store",
     "invalidInviteEmail": "Sjekk e-postadressen. Den skal se ut som navn@eksempel.no."
   },
   "invoice": {

+ 1 - 0
messages/nl/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Nog geen toegang",
     "noAccessBody": "Uw account hoort bij {workshop}, maar heeft nog geen rol, dus er is hier niets te openen. Vraag een eigenaar of beheerder van uw werkplaats om een rol.",
     "setupPageInstall": "Downloaden via Google Play",
+    "setupPageInstallIos": "Download in de App Store",
     "invalidInviteEmail": "Controleer het e-mailadres. Het moet eruitzien als naam@voorbeeld.nl."
   },
   "invoice": {

+ 1 - 0
messages/pl/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Brak dostępu",
     "noAccessBody": "Twoje konto należy do {workshop}, ale nie ma jeszcze przypisanej roli, więc nie ma tu nic, co możesz otworzyć. Poproś właściciela lub administratora warsztatu o rolę.",
     "setupPageInstall": "Pobierz z Google Play",
+    "setupPageInstallIos": "Pobierz z App Store",
     "invalidInviteEmail": "Sprawdź adres e-mail. Powinien wyglądać tak: imie@przyklad.pl."
   },
   "invoice": {

+ 1 - 0
messages/pt-BR/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ainda sem acesso",
     "noAccessBody": "Sua conta faz parte da {workshop}, mas ainda não recebeu uma função, então não há nada aqui que você possa abrir. Peça uma função ao proprietário ou a um administrador da sua oficina.",
     "setupPageInstall": "Baixar no Google Play",
+    "setupPageInstallIos": "Baixar na App Store",
     "invalidInviteEmail": "Confira o endereço de e-mail. Ele deve ter o formato nome@exemplo.com."
   },
   "invoice": {

+ 1 - 0
messages/pt-PT/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ainda sem acesso",
     "noAccessBody": "A sua conta faz parte de {workshop}, mas ainda não lhe foi atribuída uma função, por isso ainda não há nada aqui que possa abrir. Peça ao proprietário ou a um administrador da sua oficina que lhe atribua uma.",
     "setupPageInstall": "Disponível no Google Play",
+    "setupPageInstallIos": "Descarregar na App Store",
     "invalidInviteEmail": "Verifique o endereço de email. Deve ter o formato nome@exemplo.pt."
   },
   "invoice": {

+ 1 - 0
messages/ru/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Доступа пока нет",
     "noAccessBody": "Ваша учётная запись относится к {workshop}, но ей ещё не назначена роль, поэтому открыть здесь нечего. Попросите владельца или администратора мастерской назначить вам роль.",
     "setupPageInstall": "Загрузить в Google Play",
+    "setupPageInstallIos": "Загрузите в App Store",
     "invalidInviteEmail": "Проверьте адрес электронной почты. Он должен выглядеть так: name@example.com."
   },
   "invoice": {

+ 1 - 0
messages/tr/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Henüz erişim yok",
     "noAccessBody": "Hesabınız {workshop} atölyesine ait, ancak henüz bir rol verilmemiş; bu yüzden burada açabileceğiniz bir şey yok. Atölyenizdeki bir sahip ya da yöneticiden rol isteyin.",
     "setupPageInstall": "Google Play’den indirin",
+    "setupPageInstallIos": "App Store'dan indirin",
     "invalidInviteEmail": "E-posta adresini kontrol edin. ad@ornek.com gibi görünmelidir."
   },
   "invoice": {

+ 53 - 0
src/__tests__/features/team/tech-app-store-links.test.tsx

@@ -0,0 +1,53 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+vi.mock('next/image', () => ({
+  default: (props: { alt: string }) => <span>{props.alt}</span>,
+}))
+
+import { AppSetupLanding } from '@/app/(public)/app-setup/app-setup-landing'
+import { SetupCodeHandoff } from '@/features/team/Components/SetupCodeHandoff'
+
+/**
+ * Torqvoice Tech is on Google Play and the App Store. Both places that send a
+ * technician to install it offer both stores, so an iPhone is not sent to
+ * Google Play.
+ */
+
+const PLAY = 'https://play.google.com/store/apps/details?id=com.torqvoice.technician'
+const APPLE = 'https://apps.apple.com/app/id6809578893'
+
+function storeLinks() {
+  return {
+    play: screen.getByRole('link', { name: 'Get it on Google Play' }),
+    apple: screen.getByRole('link', { name: 'Download on the App Store' }),
+  }
+}
+
+describe('installing the technician app', () => {
+  it('offers both stores in the setup dialog at the desk', () => {
+    render(
+      <SetupCodeHandoff
+        issued={{
+          code: 'ABCD1234',
+          display: 'ABCD-1234',
+          expiresAt: new Date(Date.now() + 600_000).toISOString(),
+          name: 'Jake',
+        }}
+        error={null}
+        workshopUrl="https://app.example.com"
+        memberName="Jake"
+      />
+    )
+    const { play, apple } = storeLinks()
+    expect(play).toHaveAttribute('href', PLAY)
+    expect(apple).toHaveAttribute('href', APPLE)
+  })
+
+  it('offers both stores on the page a scanned code opens', () => {
+    render(<AppSetupLanding />)
+    const { play, apple } = storeLinks()
+    expect(play).toHaveAttribute('href', PLAY)
+    expect(apple).toHaveAttribute('href', APPLE)
+  })
+})

+ 18 - 10
src/app/(public)/app-setup/app-setup-landing.tsx

@@ -5,7 +5,7 @@ import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { Download, Smartphone } from 'lucide-react'
 import { Button } from '@/components/ui/button'
-import { PLAY_STORE_URL } from '@/lib/app-links'
+import { APP_STORE_URL, PLAY_STORE_URL } from '@/lib/app-links'
 
 /**
  * Reads the code out of the fragment and tells the technician what to do.
@@ -70,15 +70,23 @@ export function AppSetupLanding() {
         {/* For a phone that does not have it yet. The button above opens the
             app and does nothing visible when it is missing, which is exactly
             the moment somebody needs this. */}
-        <a
-          href={PLAY_STORE_URL}
-          target="_blank"
-          rel="noreferrer"
-          className="mt-3 inline-flex items-center gap-2 text-muted-foreground text-sm underline underline-offset-4 hover:text-foreground"
-        >
-          <Download className="h-4 w-4" />
-          {t('team.setupPageInstall')}
-        </a>
+        <div className="mt-3 flex flex-col gap-2">
+          {[
+            { href: PLAY_STORE_URL, label: t('team.setupPageInstall') },
+            { href: APP_STORE_URL, label: t('team.setupPageInstallIos') },
+          ].map((store) => (
+            <a
+              key={store.href}
+              href={store.href}
+              target="_blank"
+              rel="noreferrer"
+              className="inline-flex items-center gap-2 text-muted-foreground text-sm underline underline-offset-4 hover:text-foreground"
+            >
+              <Download className="h-4 w-4" />
+              {store.label}
+            </a>
+          ))}
+        </div>
 
         <p className="pt-6 text-muted-foreground text-xs">{t('team.setupPageExpiry')}</p>
       </div>

+ 11 - 4
src/features/team/Components/SetupCodeHandoff.tsx

@@ -3,7 +3,7 @@
 import { useEffect, useState } from 'react'
 import { useTranslations } from 'next-intl'
 import { QRCodeSVG } from 'qrcode.react'
-import { PLAY_STORE_URL } from '@/lib/app-links'
+import { APP_STORE_URL, PLAY_STORE_URL } from '@/lib/app-links'
 import { Check, Copy, Loader2 } from 'lucide-react'
 import { Button } from '@/components/ui/button'
 
@@ -87,8 +87,7 @@ export function SetupCodeHandoff({
                     the first step carries the link rather than leaving
                     somebody to search the store on a stranger's phone. */}
                 {i === 0 && (
-                  <>
-                    {' '}
+                  <span className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
                     <a
                       href={PLAY_STORE_URL}
                       target="_blank"
@@ -97,7 +96,15 @@ export function SetupCodeHandoff({
                     >
                       {t('team.setupPageInstall')}
                     </a>
-                  </>
+                    <a
+                      href={APP_STORE_URL}
+                      target="_blank"
+                      rel="noreferrer"
+                      className="underline underline-offset-4"
+                    >
+                      {t('team.setupPageInstallIos')}
+                    </a>
+                  </span>
                 )}
               </span>
             </li>

+ 3 - 0
src/lib/app-links.ts

@@ -9,5 +9,8 @@
 export const PLAY_STORE_URL =
   'https://play.google.com/store/apps/details?id=com.torqvoice.technician'
 
+/** The iPhone listing, offered next to Google Play wherever the app is. */
+export const APP_STORE_URL = 'https://apps.apple.com/app/id6809578893'
+
 /** The Android package, which is also what a deep link resolves against. */
 export const ANDROID_PACKAGE = 'com.torqvoice.technician'