Просмотр исходного кода

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

Bernt Christian Egeland 1 день назад
Родитель
Сommit
1197f7acf9

+ 1 - 0
messages/de/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Noch kein Zugriff",
     "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.",
     "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",
     "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."
     "invalidInviteEmail": "Prüfen Sie die E-Mail-Adresse. Sie sollte so aussehen: name@beispiel.de."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/en/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "No access yet",
     "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.",
     "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",
     "setupPageInstall": "Get it on Google Play",
+    "setupPageInstallIos": "Download on the App Store",
     "invalidInviteEmail": "Check the email address. It should look like name@example.com."
     "invalidInviteEmail": "Check the email address. It should look like name@example.com."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/es/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Sin acceso todavía",
     "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.",
     "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",
     "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."
     "invalidInviteEmail": "Revisa la dirección de correo. Debe tener la forma nombre@ejemplo.com."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/fr/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Pas encore d’accès",
     "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.",
     "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",
     "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."
     "invalidInviteEmail": "Vérifiez l'adresse e-mail. Elle doit ressembler à nom@exemple.fr."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/it/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ancora nessun accesso",
     "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.",
     "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",
     "setupPageInstall": "Scaricala su Google Play",
+    "setupPageInstallIos": "Scarica su App Store",
     "invalidInviteEmail": "Controlla l'indirizzo email. Deve avere la forma nome@esempio.it."
     "invalidInviteEmail": "Controlla l'indirizzo email. Deve avere la forma nome@esempio.it."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/lt/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Prieigos kol kas nėra",
     "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ę.",
     "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“",
     "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."
     "invalidInviteEmail": "Patikrinkite el. pašto adresą. Jis turėtų atrodyti taip: vardas@pavyzdys.lt."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/nb/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ingen tilgang ennå",
     "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.",
     "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",
     "setupPageInstall": "Last ned fra Google Play",
+    "setupPageInstallIos": "Last ned fra App Store",
     "invalidInviteEmail": "Sjekk e-postadressen. Den skal se ut som navn@eksempel.no."
     "invalidInviteEmail": "Sjekk e-postadressen. Den skal se ut som navn@eksempel.no."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/nl/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Nog geen toegang",
     "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.",
     "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",
     "setupPageInstall": "Downloaden via Google Play",
+    "setupPageInstallIos": "Download in de App Store",
     "invalidInviteEmail": "Controleer het e-mailadres. Het moet eruitzien als naam@voorbeeld.nl."
     "invalidInviteEmail": "Controleer het e-mailadres. Het moet eruitzien als naam@voorbeeld.nl."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/pl/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Brak dostępu",
     "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ę.",
     "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",
     "setupPageInstall": "Pobierz z Google Play",
+    "setupPageInstallIos": "Pobierz z App Store",
     "invalidInviteEmail": "Sprawdź adres e-mail. Powinien wyglądać tak: imie@przyklad.pl."
     "invalidInviteEmail": "Sprawdź adres e-mail. Powinien wyglądać tak: imie@przyklad.pl."
   },
   },
   "invoice": {
   "invoice": {

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

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ainda sem acesso",
     "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.",
     "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",
     "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."
     "invalidInviteEmail": "Confira o endereço de e-mail. Ele deve ter o formato nome@exemplo.com."
   },
   },
   "invoice": {
   "invoice": {

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

@@ -564,6 +564,7 @@
     "noAccessTitle": "Ainda sem acesso",
     "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.",
     "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",
     "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."
     "invalidInviteEmail": "Verifique o endereço de email. Deve ter o formato nome@exemplo.pt."
   },
   },
   "invoice": {
   "invoice": {

+ 1 - 0
messages/ru/settings.json

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

+ 1 - 0
messages/tr/settings.json

@@ -564,6 +564,7 @@
     "noAccessTitle": "Henüz erişim yok",
     "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.",
     "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",
     "setupPageInstall": "Google Play’den indirin",
+    "setupPageInstallIos": "App Store'dan indirin",
     "invalidInviteEmail": "E-posta adresini kontrol edin. ad@ornek.com gibi görünmelidir."
     "invalidInviteEmail": "E-posta adresini kontrol edin. ad@ornek.com gibi görünmelidir."
   },
   },
   "invoice": {
   "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 { useTranslations } from 'next-intl'
 import { Download, Smartphone } from 'lucide-react'
 import { Download, Smartphone } from 'lucide-react'
 import { Button } from '@/components/ui/button'
 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.
  * 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
         {/* 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
             app and does nothing visible when it is missing, which is exactly
             the moment somebody needs this. */}
             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>
         <p className="pt-6 text-muted-foreground text-xs">{t('team.setupPageExpiry')}</p>
       </div>
       </div>

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

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

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

@@ -9,5 +9,8 @@
 export const PLAY_STORE_URL =
 export const PLAY_STORE_URL =
   'https://play.google.com/store/apps/details?id=com.torqvoice.technician'
   '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. */
 /** The Android package, which is also what a deep link resolves against. */
 export const ANDROID_PACKAGE = 'com.torqvoice.technician'
 export const ANDROID_PACKAGE = 'com.torqvoice.technician'