feat: Apple Wallet passes for bookings

"Add to Apple Wallet" on a booking in the iOS app turns it into a TREK pass:
a boarding pass from and to for flights, trains, buses and boats, check-in and
check-out for a stay, pickup and drop-off for a car, the time for anything
else, with the confirmation code as a QR code. Wallet brings it up on the lock
screen around its time and place: times carry the offset of the endpoint's zone
(looked up from the coordinates where the endpoint has none), stays and
restaurants their place's coordinates. The labels come from the booking forms'
own strings, so nothing new to translate on the pass.

Only liketrek holds the Pass Type ID certificate, so the server sends the pass's
texts to the TREK services' signer (TREK_WALLET_PASS_URL, push.liketrek.com)
once per tap; it stores nothing, and TREK_SERVICES=off turns it off with the
other services. Texts are cleaned of links and shortened here, the answer must
be a .pkpass within 512 KB, and a user gets 20 passes a minute.

- New wallet-pass domain: GET /api/trips/:tripId/reservations/:id/wallet-pass
  behind the trip guard, the booking checked to belong to the trip; trip title
  and place come from the reservations domain (findPassContext)
- MWalletButton in the transport and booking sheets, only where the app binary
  has the wallet feature; undated bookings and failures are told apart
- TREK_WALLET_PASS_URL in .env.example, compose, Helm and the wiki
This commit is contained in:
Maurice
2026-11-07 10:40:41 +01:00
parent 794da3be72
commit 37a4e84360
56 changed files with 1873 additions and 113 deletions
+3
View File
@@ -299,5 +299,8 @@ const reservations: TranslationStrings = {
'يحتاج رفع الملفات إلى اتصال. أزل الملفات الجديدة لحفظ الحجز دون اتصال، أو احفظ عندما تعود متصلاً.',
'reservations.needsConnection': 'يتطلب هذا اتصالاً. حاول مرة أخرى عندما يعود TREK للاتصال.',
'reservations.exportCsvFiltered': 'تصدير الصفوف المعروضة ({shown} من {total}) (CSV)',
'reservations.wallet.add': 'إضافة إلى Apple Wallet',
'reservations.wallet.failed': 'تعذّر إنشاء بطاقة Wallet. حاول مرة أخرى.',
'reservations.wallet.undated': 'حدّد تاريخًا للحجز أولًا: يعرض Wallet البطاقة قرب موعده.',
};
export default reservations;
+3
View File
@@ -307,6 +307,9 @@ const reservations: TranslationStrings = {
'Faylların yüklənməsi üçün bağlantı lazımdır. Rezervasiyanı oflayn saxlamaq üçün yeni faylları silin və ya yenidən onlayn olduqda saxlayın.',
'reservations.needsConnection': 'Bunun üçün bağlantı lazımdır. TREK yenidən onlayn olduqda bir daha cəhd edin.',
'reservations.exportCsvFiltered': 'Göstərilən {shown}/{total} sətri ixrac et (CSV)',
'reservations.wallet.add': 'Apple Wallet-ə əlavə et',
'reservations.wallet.failed': 'Wallet kartını yaratmaq alınmadı. Yenidən cəhd edin.',
'reservations.wallet.undated': 'Əvvəlcə rezervasiyaya tarix verin: Wallet kartı həmin vaxta yaxın göstərir.',
};
export default reservations;
+3
View File
@@ -273,5 +273,8 @@ const reservations: TranslationStrings = {
'Os arquivos precisam de conexão para serem enviados. Remova os novos para salvar a reserva offline, ou salve quando estiver online de novo.',
'reservations.needsConnection': 'Isso precisa de conexão. Tente de novo quando o TREK voltar a ficar online.',
'reservations.exportCsvFiltered': 'Ezporzhiañ ar {shown} diwar {total} renk skwer (CSV)',
'reservations.wallet.add': 'Adicionar à Apple Wallet',
'reservations.wallet.failed': 'Não foi possível criar o passe da Wallet. Tente novamente.',
'reservations.wallet.undated': 'Primeiro dê uma data à reserva: a Wallet mostra o passe perto desse horário.',
};
export default reservations;
+4
View File
@@ -276,5 +276,9 @@ const reservations: TranslationStrings = {
'Els fitxers necessiten connexió per pujar-se. Treu els nous per desar la reserva sense connexió, o desa quan tornis a estar en línia.',
'reservations.needsConnection': 'Això necessita connexió. Torna-ho a provar quan TREK torni a estar en línia.',
'reservations.exportCsvFiltered': 'Exporta les {shown} de {total} files mostrades (CSV)',
'reservations.wallet.add': 'Afegeix a Apple Wallet',
'reservations.wallet.failed': 'No s’ha pogut crear el passi de Wallet. Torna-ho a provar.',
'reservations.wallet.undated':
'Primer posa una data a la reserva: Wallet mostra el passi al voltant d’aquell moment.',
};
export default reservations;
+3
View File
@@ -280,5 +280,8 @@ const reservations: TranslationStrings = {
'Soubory potřebují k nahrání připojení. Odeber nové, abys rezervaci uložil offline, nebo ulož, až budeš znovu online.',
'reservations.needsConnection': 'Na tohle je potřeba připojení. Zkus to znovu, až bude TREK zase online.',
'reservations.exportCsvFiltered': 'Exportovat zobrazené řádky, {shown} z {total} (CSV)',
'reservations.wallet.add': 'Přidat do Apple Wallet',
'reservations.wallet.failed': 'Kartu do Wallet se nepodařilo vytvořit. Zkuste to znovu.',
'reservations.wallet.undated': 'Nejdřív rezervaci zadejte datum: Wallet kartu zobrazí kolem toho času.',
};
export default reservations;
+3
View File
@@ -278,5 +278,8 @@ const reservations: TranslationStrings = {
'reservations.needsConnection':
'Dafür braucht es eine Verbindung. Versuch es noch einmal, sobald TREK wieder online ist.',
'reservations.exportCsvFiltered': 'Die {shown} von {total} angezeigten Zeilen exportieren (CSV)',
'reservations.wallet.add': 'Zu Apple Wallet hinzufügen',
'reservations.wallet.failed': 'Der Wallet-Pass konnte nicht erstellt werden. Bitte versuch es erneut.',
'reservations.wallet.undated': 'Gib der Buchung zuerst ein Datum: Wallet zeigt den Pass rund um diesen Zeitpunkt an.',
};
export default reservations;
+3
View File
@@ -275,5 +275,8 @@ const reservations: TranslationStrings = {
'Files need a connection to upload. Remove the new ones to save the booking offline, or save once you are back online.',
'reservations.needsConnection': 'This needs a connection. Try again once TREK is back online.',
'reservations.exportCsvFiltered': 'Export the {shown} of {total} shown rows (CSV)',
'reservations.wallet.add': 'Add to Apple Wallet',
'reservations.wallet.failed': 'The Wallet pass could not be made. Please try again.',
'reservations.wallet.undated': 'Give the booking a date first: Wallet brings the pass up around its time.',
};
export default reservations;
+3
View File
@@ -276,5 +276,8 @@ const reservations: TranslationStrings = {
'Los archivos necesitan conexión para subirse. Quita los nuevos para guardar la reserva sin conexión, o guarda cuando vuelvas a estar en línea.',
'reservations.needsConnection': 'Esto necesita conexión. Vuelve a intentarlo cuando TREK esté de nuevo en línea.',
'reservations.exportCsvFiltered': 'Exportar las {shown} de {total} filas mostradas (CSV)',
'reservations.wallet.add': 'Añadir a Apple Wallet',
'reservations.wallet.failed': 'No se pudo crear el pase de Wallet. Inténtalo de nuevo.',
'reservations.wallet.undated': 'Primero ponle una fecha a la reserva: Wallet muestra el pase en torno a ese momento.',
};
export default reservations;
+3
View File
@@ -275,5 +275,8 @@ const reservations: TranslationStrings = {
'Failide üleslaadimiseks on vaja ühendust. Eemalda uued, et broneering võrguühenduseta salvestada, või salvesta, kui oled taas võrgus.',
'reservations.needsConnection': 'Selleks on vaja ühendust. Proovi uuesti, kui TREK on taas võrgus.',
'reservations.exportCsvFiltered': 'Ekspordi näidatud read, {shown} / {total} (CSV)',
'reservations.wallet.add': 'Lisa Apple Walletisse',
'reservations.wallet.failed': 'Walleti passi ei õnnestunud luua. Proovi uuesti.',
'reservations.wallet.undated': 'Anna broneeringule kõigepealt kuupäev: Wallet näitab passi selle aja paiku.',
};
export default reservations;
+4
View File
@@ -279,5 +279,9 @@ const reservations: TranslationStrings = {
"Les fichiers ont besoin d'une connexion pour être envoyés. Retire les nouveaux pour enregistrer la réservation hors ligne, ou enregistre une fois de retour en ligne.",
'reservations.needsConnection': 'Cela nécessite une connexion. Réessaie dès que TREK sera de nouveau en ligne.',
'reservations.exportCsvFiltered': 'Exporter les {shown} lignes affichées sur {total} (CSV)',
'reservations.wallet.add': 'Ajouter à Apple Wallet',
'reservations.wallet.failed': 'Impossible de créer la carte Wallet. Réessayez.',
'reservations.wallet.undated':
'Donnez d’abord une date à la réservation : Wallet affiche la carte autour de ce moment.',
};
export default reservations;
+4
View File
@@ -278,5 +278,9 @@ const reservations: TranslationStrings = {
'Τα αρχεία χρειάζονται σύνδεση για να ανέβουν. Αφαίρεσε τα νέα για να αποθηκεύσεις την κράτηση εκτός σύνδεσης ή αποθήκευσε όταν είσαι ξανά online.',
'reservations.needsConnection': 'Αυτό χρειάζεται σύνδεση. Δοκίμασε ξανά μόλις το TREK συνδεθεί ξανά.',
'reservations.exportCsvFiltered': 'Εξαγωγή των {shown} από {total} εμφανιζόμενων γραμμών (CSV)',
'reservations.wallet.add': 'Προσθήκη στο Apple Wallet',
'reservations.wallet.failed': 'Δεν ήταν δυνατή η δημιουργία της κάρτας Wallet. Δοκιμάστε ξανά.',
'reservations.wallet.undated':
'Δώστε πρώτα ημερομηνία στην κράτηση: το Wallet εμφανίζει την κάρτα γύρω από εκείνη την ώρα.',
};
export default reservations;
+3
View File
@@ -277,6 +277,9 @@ const reservations: TranslationStrings = {
'העלאת קבצים דורשת חיבור. הסירו את הקבצים החדשים כדי לשמור את ההזמנה במצב לא מקוון, או שמרו כשתחזרו לרשת.',
'reservations.needsConnection': 'הפעולה הזו דורשת חיבור. נסו שוב כש-TREK יחזור לרשת.',
'reservations.exportCsvFiltered': 'ייצוא {shown} מתוך {total} השורות המוצגות (CSV)',
'reservations.wallet.add': 'הוספה ל-Apple Wallet',
'reservations.wallet.failed': 'לא ניתן היה ליצור את כרטיס ה-Wallet. אנא נסו שוב.',
'reservations.wallet.undated': 'קודם תנו להזמנה תאריך: Wallet מציג את הכרטיס סביב המועד שלה.',
};
export default reservations;
+3
View File
@@ -277,5 +277,8 @@ const reservations: TranslationStrings = {
'A fájlok feltöltéséhez kapcsolat kell. Távolítsd el az újakat, hogy offline mentsd a foglalást, vagy ments, amikor újra online vagy.',
'reservations.needsConnection': 'Ehhez kapcsolat kell. Próbáld újra, amint a TREK újra online lesz.',
'reservations.exportCsvFiltered': 'A megjelenített {shown} sor exportálása a(z) {total} közül (CSV)',
'reservations.wallet.add': 'Hozzáadás az Apple Wallethez',
'reservations.wallet.failed': 'Nem sikerült létrehozni a Wallet-kártyát. Próbáld újra.',
'reservations.wallet.undated': 'Előbb adj dátumot a foglalásnak: a Wallet az időpont körül jeleníti meg a kártyát.',
};
export default reservations;
+3
View File
@@ -266,5 +266,8 @@ const reservations: TranslationStrings = {
'File perlu koneksi untuk diunggah. Hapus file baru untuk menyimpan pemesanan secara offline, atau simpan saat kamu kembali online.',
'reservations.needsConnection': 'Ini memerlukan koneksi. Coba lagi begitu TREK kembali online.',
'reservations.exportCsvFiltered': 'Ekspor {shown} dari {total} baris yang ditampilkan (CSV)',
'reservations.wallet.add': 'Tambahkan ke Apple Wallet',
'reservations.wallet.failed': 'Pass Wallet tidak dapat dibuat. Silakan coba lagi.',
'reservations.wallet.undated': 'Beri tanggal pada pemesanan dulu: Wallet menampilkan pass di sekitar waktunya.',
};
export default reservations;
+3
View File
@@ -274,5 +274,8 @@ const reservations: TranslationStrings = {
'I file hanno bisogno di una connessione per essere caricati. Rimuovi quelli nuovi per salvare la prenotazione offline, oppure salva quando sei di nuovo online.',
'reservations.needsConnection': 'Serve una connessione. Riprova quando TREK tornerà online.',
'reservations.exportCsvFiltered': 'Esporta le {shown} righe mostrate su {total} (CSV)',
'reservations.wallet.add': 'Aggiungi ad Apple Wallet',
'reservations.wallet.failed': 'Impossibile creare il pass di Wallet. Riprova.',
'reservations.wallet.undated': 'Prima dai una data alla prenotazione: Wallet mostra il pass intorno a quel momento.',
};
export default reservations;
+3
View File
@@ -266,5 +266,8 @@ const reservations: TranslationStrings = {
'ファイルのアップロードには接続が必要です。オフラインで予約を保存するには新しいファイルを外すか、オンラインに戻ってから保存してください。',
'reservations.needsConnection': 'この操作には接続が必要です。TREK がオンラインに戻ってから、もう一度お試しください。',
'reservations.exportCsvFiltered': '表示中の {shown}/{total} 行をエクスポート (CSV)',
'reservations.wallet.add': 'Apple Walletに追加',
'reservations.wallet.failed': 'Walletパスを作成できませんでした。もう一度お試しください。',
'reservations.wallet.undated': '先に予約に日付を入れてください。Walletはその時刻の前後にパスを表示します。',
};
export default reservations;
+3
View File
@@ -266,5 +266,8 @@ const reservations: TranslationStrings = {
'파일을 업로드하려면 연결이 필요합니다. 오프라인으로 예약을 저장하려면 새 파일을 제거하거나, 다시 온라인이 되면 저장하세요.',
'reservations.needsConnection': '연결이 필요한 작업입니다. TREK가 다시 온라인이 되면 다시 시도하세요.',
'reservations.exportCsvFiltered': '표시된 {total}행 중 {shown}행 내보내기 (CSV)',
'reservations.wallet.add': 'Apple Wallet에 추가',
'reservations.wallet.failed': 'Wallet 패스를 만들지 못했습니다. 다시 시도하세요.',
'reservations.wallet.undated': '먼저 예약에 날짜를 지정하세요. Wallet은 그 시간 즈음에 패스를 보여 줍니다.',
};
export default reservations;
+3
View File
@@ -275,5 +275,8 @@ const reservations: TranslationStrings = {
'Bestanden hebben een verbinding nodig om te uploaden. Verwijder de nieuwe om de boeking offline op te slaan, of sla op zodra je weer online bent.',
'reservations.needsConnection': 'Hiervoor is een verbinding nodig. Probeer het opnieuw zodra TREK weer online is.',
'reservations.exportCsvFiltered': 'De {shown} van {total} getoonde rijen exporteren (CSV)',
'reservations.wallet.add': 'Voeg toe aan Apple Wallet',
'reservations.wallet.failed': 'De Wallet-pas kon niet worden gemaakt. Probeer het opnieuw.',
'reservations.wallet.undated': 'Geef de boeking eerst een datum: Wallet toont de pas rond dat moment.',
};
export default reservations;
+3
View File
@@ -289,5 +289,8 @@ const reservations: TranslationStrings = {
'Pliki wymagają połączenia do przesłania. Usuń nowe, aby zapisać rezerwację offline, albo zapisz po powrocie online.',
'reservations.needsConnection': 'To wymaga połączenia. Spróbuj ponownie, gdy TREK znów będzie online.',
'reservations.exportCsvFiltered': 'Eksportuj wyświetlone wiersze: {shown} z {total} (CSV)',
'reservations.wallet.add': 'Dodaj do Apple Wallet',
'reservations.wallet.failed': 'Nie udało się utworzyć karty Wallet. Spróbuj ponownie.',
'reservations.wallet.undated': 'Najpierw nadaj rezerwacji datę: Wallet pokazuje kartę w okolicach tego terminu.',
};
export default reservations;
+3
View File
@@ -287,5 +287,8 @@ const reservations: TranslationStrings = {
'Для загрузки файлов нужно подключение. Удалите новые, чтобы сохранить бронирование офлайн, или сохраните, когда снова будете онлайн.',
'reservations.needsConnection': 'Для этого нужно подключение. Попробуйте ещё раз, когда TREK снова будет онлайн.',
'reservations.exportCsvFiltered': 'Экспортировать показанные строки: {shown} из {total} (CSV)',
'reservations.wallet.add': 'Добавить в Apple Wallet',
'reservations.wallet.failed': 'Не удалось создать карту Wallet. Повторите попытку.',
'reservations.wallet.undated': 'Сначала укажите дату брони: Wallet показывает карту ближе к этому времени.',
};
export default reservations;
+3
View File
@@ -283,5 +283,8 @@ const reservations: TranslationStrings = {
'Súbory potrebujú na nahratie pripojenie. Odstráň nové, aby si rezerváciu uložil offline, alebo ulož, keď budeš znova online.',
'reservations.needsConnection': 'Na toto je potrebné pripojenie. Skús to znova, keď bude TREK opäť online.',
'reservations.exportCsvFiltered': 'Exportovať zobrazené riadky, {shown} z {total} (CSV)',
'reservations.wallet.add': 'Pridať do Apple Wallet',
'reservations.wallet.failed': 'Kartu do Wallet sa nepodarilo vytvoriť. Skúste to znova.',
'reservations.wallet.undated': 'Najprv rezervácii zadajte dátum: Wallet kartu zobrazí okolo toho času.',
};
export default reservations;
+3
View File
@@ -274,5 +274,8 @@ const reservations: TranslationStrings = {
'Filer behöver en anslutning för att laddas upp. Ta bort de nya för att spara bokningen offline, eller spara när du är online igen.',
'reservations.needsConnection': 'Det här kräver en anslutning. Försök igen när TREK är online igen.',
'reservations.exportCsvFiltered': 'Exportera de {shown} av {total} visade raderna (CSV)',
'reservations.wallet.add': 'Lägg till i Apple Wallet',
'reservations.wallet.failed': 'Wallet-kortet kunde inte skapas. Försök igen.',
'reservations.wallet.undated': 'Ge bokningen ett datum först: Wallet visar kortet runt den tiden.',
};
export default reservations;
+3
View File
@@ -268,5 +268,8 @@ const reservations: TranslationStrings = {
'การอัปโหลดไฟล์ต้องใช้การเชื่อมต่อ ลบไฟล์ใหม่ออกเพื่อบันทึกการจองแบบออฟไลน์ หรือบันทึกเมื่อกลับมาออนไลน์',
'reservations.needsConnection': 'การทำงานนี้ต้องใช้การเชื่อมต่อ ลองอีกครั้งเมื่อ TREK กลับมาออนไลน์',
'reservations.exportCsvFiltered': 'ส่งออก {shown} จาก {total} แถวที่แสดง (CSV)',
'reservations.wallet.add': 'เพิ่มลงใน Apple Wallet',
'reservations.wallet.failed': 'สร้างบัตร Wallet ไม่สำเร็จ โปรดลองอีกครั้ง',
'reservations.wallet.undated': 'กำหนดวันที่ให้การจองก่อน Wallet จะแสดงบัตรในช่วงเวลานั้น',
};
export default reservations;
+3
View File
@@ -275,5 +275,8 @@ const reservations: TranslationStrings = {
'Dosyaların yüklenmesi için bağlantı gerekir. Rezervasyonu çevrimdışı kaydetmek için yenilerini kaldır ya da tekrar çevrimiçi olduğunda kaydet.',
'reservations.needsConnection': 'Bunun için bağlantı gerekiyor. TREK yeniden çevrimiçi olduğunda tekrar dene.',
'reservations.exportCsvFiltered': 'Gösterilen {total} satırdan {shown} tanesini dışa aktar (CSV)',
'reservations.wallet.add': 'Apple Cüzdan’a Ekle',
'reservations.wallet.failed': 'Cüzdan kartı oluşturulamadı. Lütfen tekrar deneyin.',
'reservations.wallet.undated': 'Önce rezervasyona bir tarih verin: Cüzdan kartı o zamana yakın gösterir.',
};
export default reservations;
+3
View File
@@ -287,5 +287,8 @@ const reservations: TranslationStrings = {
"Для завантаження файлів потрібне з'єднання. Видаліть нові, щоб зберегти бронювання офлайн, або збережіть, коли знову будете онлайн.",
'reservations.needsConnection': "Для цього потрібне з'єднання. Спробуйте ще раз, коли TREK знову буде онлайн.",
'reservations.exportCsvFiltered': 'Експортувати показані рядки: {shown} із {total} (CSV)',
'reservations.wallet.add': 'Додати до Apple Wallet',
'reservations.wallet.failed': 'Не вдалося створити картку Wallet. Повторіть спробу.',
'reservations.wallet.undated': 'Спершу вкажіть дату бронювання: Wallet показує картку ближче до цього часу.',
};
export default reservations;
+3
View File
@@ -269,5 +269,8 @@ const reservations: TranslationStrings = {
'Cần có kết nối để tải tệp lên. Gỡ các tệp mới để lưu đặt chỗ khi ngoại tuyến, hoặc lưu khi bạn trực tuyến trở lại.',
'reservations.needsConnection': 'Thao tác này cần kết nối. Hãy thử lại khi TREK trực tuyến trở lại.',
'reservations.exportCsvFiltered': 'Xuất {shown} trong {total} dòng đang hiển thị (CSV)',
'reservations.wallet.add': 'Thêm vào Apple Wallet',
'reservations.wallet.failed': 'Không thể tạo thẻ Wallet. Vui lòng thử lại.',
'reservations.wallet.undated': 'Hãy đặt ngày cho đặt chỗ trước: Wallet sẽ hiện thẻ vào khoảng thời gian đó.',
};
export default reservations;
+3
View File
@@ -261,5 +261,8 @@ const reservations: TranslationStrings = {
'reservations.filesNeedConnection': '上傳檔案需要網路連線。移除新檔案即可離線儲存這筆預訂,或在恢復連線後再儲存。',
'reservations.needsConnection': '此操作需要網路連線。請在 TREK 恢復連線後再試一次。',
'reservations.exportCsvFiltered': '匯出顯示的 {shown}/{total} 列 (CSV)',
'reservations.wallet.add': '加入 Apple 錢包',
'reservations.wallet.failed': '無法產生錢包票卡。請重試。',
'reservations.wallet.undated': '請先為訂位設定日期:錢包會在那個時間前後顯示票卡。',
};
export default reservations;
+3
View File
@@ -261,5 +261,8 @@ const reservations: TranslationStrings = {
'reservations.filesNeedConnection': '上传文件需要网络连接。移除新文件即可离线保存这条预订,或在恢复联网后再保存。',
'reservations.needsConnection': '此操作需要网络连接。请在 TREK 恢复联网后重试。',
'reservations.exportCsvFiltered': '导出显示的 {shown}/{total} 行 (CSV)',
'reservations.wallet.add': '添加到 Apple 钱包',
'reservations.wallet.failed': '无法生成钱包卡券。请重试。',
'reservations.wallet.undated': '请先为预订设置日期:钱包会在那个时间前后显示卡券。',
};
export default reservations;