Add Zapstore app download nudge

Prompt mobile-web visitors to install the native Android app from
Zapstore. Shows a card at the bottom of the home feed and a link in
the account switcher menu. Both are hidden inside the native app
(Capacitor.isNativePlatform) and on desktop (sm:hidden for the banner).

Adds nav.getApp and feed.getApp.* strings across all locales.
This commit is contained in:
Chad Curtis
2026-06-02 08:40:58 -05:00
parent 4153792e54
commit e7c488af63
20 changed files with 200 additions and 18 deletions
+49
View File
@@ -0,0 +1,49 @@
import { Capacitor } from '@capacitor/core';
import { useTranslation } from 'react-i18next';
import { ArrowRight } from 'lucide-react';
import { useAppContext } from '@/hooks/useAppContext';
import { ZAPSTORE_URL } from '@/lib/zapstore';
/**
* Zapstore download nudge — prompts mobile-web visitors to install the native
* Android app. Hidden inside the native app (you're already in it) and on
* desktop (`sm:hidden`), where downloading works differently.
*/
export function AppDownloadNudge() {
const { t } = useTranslation();
const { config } = useAppContext();
if (Capacitor.isNativePlatform()) return null;
return (
<div className="sm:hidden px-4 pt-8 pb-4">
<p className="text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-3">
{t('feed.getApp.eyebrow')}
</p>
<div className="flex items-center gap-3">
<img
src="/logo.png"
alt={config.appName}
className="h-10 w-10 shrink-0 rounded-xl"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground">
{t('feed.getApp.title', { appName: config.appName })}
</p>
<p className="text-xs text-muted-foreground">
{t('feed.getApp.subtitle', { appName: config.appName })}
</p>
</div>
<a
href={ZAPSTORE_URL}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary/10 hover:bg-primary/20 text-primary text-xs font-medium transition-colors"
>
{t('feed.getApp.download')}
<ArrowRight className="h-3 w-3" />
</a>
</div>
</div>
);
}
+11 -1
View File
@@ -7,8 +7,10 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Activity, Bell, ChevronDown, CircleHelp, LayoutDashboard, LogOut, Search, Settings, User, UserIcon, UserPlus, Wallet } from 'lucide-react';
import { Capacitor } from '@capacitor/core';
import { Activity, Bell, ChevronDown, CircleHelp, Download, LayoutDashboard, LogOut, Search, Settings, User, UserIcon, UserPlus, Wallet } from 'lucide-react';
import { nip19 } from 'nostr-tools';
import { ZAPSTORE_URL } from '@/lib/zapstore';
import {
DropdownMenu,
DropdownMenuContent,
@@ -144,6 +146,14 @@ export function AccountSwitcher({ onAddAccountClick }: AccountSwitcherProps) {
<span>{t('nav.about')}</span>
</Link>
</DropdownMenuItem>
{!Capacitor.isNativePlatform() && (
<DropdownMenuItem asChild className='flex items-center gap-2 cursor-pointer p-2 rounded-md'>
<a href={ZAPSTORE_URL} target="_blank" rel="noopener noreferrer">
<Download className='w-4 h-4' />
<span>{t('nav.getApp')}</span>
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onAddAccountClick}
+5
View File
@@ -0,0 +1,5 @@
/** Zapstore app id for the Android build. */
export const ZAPSTORE_APP_ID = 'spot.agora.app';
/** Public Zapstore page for the Android app. */
export const ZAPSTORE_URL = `https://zapstore.dev/apps/${encodeURIComponent(ZAPSTORE_APP_ID)}`;
+8 -1
View File
@@ -60,7 +60,8 @@
"privacy": "الخصوصية",
"safety": "السلامة",
"changelog": "سجل التغييرات",
"sourceCode": "الكود المصدري"
"sourceCode": "الكود المصدري",
"getApp": "احصل على التطبيق"
},
"auth": {
"join": "انضمام",
@@ -128,6 +129,12 @@
},
"feed": {
"indexTagline": "محتواك. أسلوبك. قواعدك.",
"getApp": {
"eyebrow": "احصل على التطبيق",
"title": "{{appName}} لنظام أندرويد",
"subtitle": "تجربة {{appName}} الكاملة",
"download": "تنزيل"
},
"compose": {
"placeholder": "ماذا يحدث؟"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Privacy",
"safety": "Safety",
"changelog": "Changelog",
"sourceCode": "Source code"
"sourceCode": "Source code",
"getApp": "Get the app"
},
"auth": {
"join": "Join",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Your content. Your vibe. Your rules.",
"getApp": {
"eyebrow": "Get the app",
"title": "{{appName}} for Android",
"subtitle": "The full {{appName}} experience",
"download": "Download"
},
"compose": {
"placeholder": "What's happening?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Privacidad",
"safety": "Seguridad",
"changelog": "Novedades",
"sourceCode": "Código fuente"
"sourceCode": "Código fuente",
"getApp": "Descargar la app"
},
"auth": {
"join": "Unirse",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Tu contenido. Tu estilo. Tus reglas.",
"getApp": {
"eyebrow": "Descargar la app",
"title": "{{appName}} para Android",
"subtitle": "La experiencia completa de {{appName}}",
"download": "Descargar"
},
"compose": {
"placeholder": "¿Qué está pasando?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "حریم خصوصی",
"safety": "ایمنی",
"changelog": "تغییرات",
"sourceCode": "کد منبع"
"sourceCode": "کد منبع",
"getApp": "دریافت برنامه"
},
"auth": {
"join": "پیوستن",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "محتوای شما. سبک شما. قوانین شما.",
"getApp": {
"eyebrow": "دریافت برنامه",
"title": "{{appName}} برای اندروید",
"subtitle": "تجربه کامل {{appName}}",
"download": "دانلود"
},
"compose": {
"placeholder": "چه خبر؟"
},
+8 -1
View File
@@ -63,7 +63,8 @@
"privacy": "Confidentialité",
"safety": "Sécurité",
"changelog": "Journal des modifications",
"sourceCode": "Code source"
"sourceCode": "Code source",
"getApp": "Télécharger l'application"
},
"auth": {
"join": "Rejoindre",
@@ -131,6 +132,12 @@
},
"feed": {
"indexTagline": "Votre contenu. Votre ambiance. Vos règles.",
"getApp": {
"eyebrow": "Télécharger l'application",
"title": "{{appName}} pour Android",
"subtitle": "L'expérience {{appName}} complète",
"download": "Télécharger"
},
"compose": {
"placeholder": "Que se passe-t-il ?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "प्राइवेसी",
"safety": "सुरक्षा",
"changelog": "बदलाव",
"sourceCode": "सोर्स कोड"
"sourceCode": "सोर्स कोड",
"getApp": "ऐप पाएं"
},
"auth": {
"join": "जुड़ें",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "आपका कंटेंट। आपका अंदाज़। आपके नियम।",
"getApp": {
"eyebrow": "ऐप पाएं",
"title": "Android के लिए {{appName}}",
"subtitle": "संपूर्ण {{appName}} अनुभव",
"download": "डाउनलोड करें"
},
"compose": {
"placeholder": "क्या चल रहा है?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Privasi",
"safety": "Keamanan",
"changelog": "Catatan Versi",
"sourceCode": "Kode sumber"
"sourceCode": "Kode sumber",
"getApp": "Dapatkan aplikasi"
},
"auth": {
"join": "Gabung",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Konten Anda. Gaya Anda. Aturan Anda.",
"getApp": {
"eyebrow": "Dapatkan aplikasi",
"title": "{{appName}} untuk Android",
"subtitle": "Pengalaman {{appName}} sepenuhnya",
"download": "Unduh"
},
"compose": {
"placeholder": "Apa yang sedang terjadi?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "ភាពឯកជន",
"safety": "សុវត្ថិភាព",
"changelog": "កំណត់ហេតុការផ្លាស់ប្តូរ",
"sourceCode": "កូដប្រភព"
"sourceCode": "កូដប្រភព",
"getApp": "ទាញយកកម្មវិធី"
},
"auth": {
"join": "ចូលរួម",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "ខ្លឹមសាររបស់អ្នក។ ស្ទីលរបស់អ្នក។ ច្បាប់របស់អ្នក។",
"getApp": {
"eyebrow": "ទាញយកកម្មវិធី",
"title": "{{appName}} សម្រាប់ Android",
"subtitle": "បទពិសោធន៍ {{appName}} ពេញលេញ",
"download": "ទាញយក"
},
"compose": {
"placeholder": "មានរឿងអ្វី?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "محرمیت",
"safety": "خوندیتوب",
"changelog": "د بدلونونو لاګ",
"sourceCode": "د سرچینې کوډ"
"sourceCode": "د سرچینې کوډ",
"getApp": "اپ ترلاسه کړئ"
},
"auth": {
"join": "ګډون",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "ستاسو مینځپانګه. ستاسو سټایل. ستاسو قواعد.",
"getApp": {
"eyebrow": "اپ ترلاسه کړئ",
"title": "{{appName}} د اندروید لپاره",
"subtitle": "بشپړ {{appName}} تجربه",
"download": "ډاونلوډ"
},
"compose": {
"placeholder": "څه روان دي؟"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Privacidade",
"safety": "Segurança",
"changelog": "Notas de versão",
"sourceCode": "Código-fonte"
"sourceCode": "Código-fonte",
"getApp": "Baixar o app"
},
"auth": {
"join": "Entrar",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Seu conteúdo. Seu estilo. Suas regras.",
"getApp": {
"eyebrow": "Baixar o app",
"title": "{{appName}} para Android",
"subtitle": "A experiência completa do {{appName}}",
"download": "Baixar"
},
"compose": {
"placeholder": "O que está acontecendo?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Конфиденциальность",
"safety": "Безопасность",
"changelog": "История изменений",
"sourceCode": "Исходный код"
"sourceCode": "Исходный код",
"getApp": "Скачать приложение"
},
"auth": {
"join": "Присоединиться",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Ваш контент. Ваша атмосфера. Ваши правила.",
"getApp": {
"eyebrow": "Скачать приложение",
"title": "{{appName}} для Android",
"subtitle": "Полный опыт {{appName}}",
"download": "Скачать"
},
"compose": {
"placeholder": "Что происходит?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "Akavanzika",
"safety": "Kuchengetedzeka",
"changelog": "Rondedzero yeshanduko",
"sourceCode": "Kodhi yetsime"
"sourceCode": "Kodhi yetsime",
"getApp": "Tora app"
},
"auth": {
"join": "Joinha",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "Zviri zvako. Mafambisirwo ako. Mitemo yako.",
"getApp": {
"eyebrow": "Tora app",
"title": "{{appName}} yeAndroid",
"subtitle": "Ruzivo rwakazara rwe{{appName}}",
"download": "Dhaunirodha"
},
"compose": {
"placeholder": "Chii chiri kuitika?"
},
+8 -1
View File
@@ -63,7 +63,8 @@
"privacy": "Faragha",
"safety": "Usalama",
"changelog": "Kumbukumbu ya mabadiliko",
"sourceCode": "Msimbo wa chanzo"
"sourceCode": "Msimbo wa chanzo",
"getApp": "Pata programu"
},
"auth": {
"join": "Jiunge",
@@ -131,6 +132,12 @@
},
"feed": {
"indexTagline": "Maudhui yako. Mtindo wako. Sheria zako.",
"getApp": {
"eyebrow": "Pata programu",
"title": "{{appName}} kwa Android",
"subtitle": "Uzoefu kamili wa {{appName}}",
"download": "Pakua"
},
"compose": {
"placeholder": "Kuna nini?"
},
+8 -1
View File
@@ -63,7 +63,8 @@
"privacy": "Gizlilik",
"safety": "Güvenlik",
"changelog": "Sürüm notları",
"sourceCode": "Kaynak kod"
"sourceCode": "Kaynak kod",
"getApp": "Uygulamayı edinin"
},
"auth": {
"join": "Katıl",
@@ -131,6 +132,12 @@
},
"feed": {
"indexTagline": "İçeriğin senin. Tarzın senin. Kuralların senin.",
"getApp": {
"eyebrow": "Uygulamayı edinin",
"title": "Android için {{appName}}",
"subtitle": "Tüm {{appName}} deneyimi",
"download": "İndir"
},
"compose": {
"placeholder": "Neler oluyor?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "隱私",
"safety": "安全",
"changelog": "更新日誌",
"sourceCode": "原始碼"
"sourceCode": "原始碼",
"getApp": "取得應用程式"
},
"auth": {
"join": "加入",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "你的內容。你的風格。你的規則。",
"getApp": {
"eyebrow": "取得應用程式",
"title": "適用於 Android 的 {{appName}}",
"subtitle": "完整的 {{appName}} 體驗",
"download": "下載"
},
"compose": {
"placeholder": "有什麼新鮮事?"
},
+8 -1
View File
@@ -64,7 +64,8 @@
"privacy": "隐私",
"safety": "安全",
"changelog": "更新日志",
"sourceCode": "源代码"
"sourceCode": "源代码",
"getApp": "获取应用"
},
"auth": {
"join": "加入",
@@ -132,6 +133,12 @@
},
"feed": {
"indexTagline": "你的内容。你的风格。你的规则。",
"getApp": {
"eyebrow": "获取应用",
"title": "适用于 Android 的 {{appName}}",
"subtitle": "完整的 {{appName}} 体验",
"download": "下载"
},
"compose": {
"placeholder": "有什么新鲜事?"
},
+7 -1
View File
@@ -1,6 +1,7 @@
import { useSeoMeta } from '@unhead/react';
import { useTranslation } from 'react-i18next';
import { Feed } from '@/components/Feed';
import { AppDownloadNudge } from '@/components/AppDownloadNudge';
import { useAppContext } from '@/hooks/useAppContext';
const Index = () => {
@@ -12,7 +13,12 @@ const Index = () => {
description: t('feed.indexTagline'),
});
return <Feed />;
return (
<>
<Feed />
<AppDownloadNudge />
</>
);
};
export default Index;