From 7e264dca37716813447c1090deda9e82300a2141 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Thu, 20 Aug 2026 21:49:05 +0900 Subject: [PATCH] feat(mobile): overhaul mobile app with Pro paywall, rewarded video refill, iOS permissions, and build APK --- apps/mobile-rn/App.tsx | 6 + apps/mobile-rn/android/gradle.properties | 7 +- apps/mobile-rn/ios/D3ROVoice/Info.plist | 4 + apps/mobile-rn/src/screens/DashScreen.tsx | 51 +- .../src/screens/ProPaywallScreen.tsx | 435 ++++++++++++++++++ apps/mobile-rn/src/screens/SettingsScreen.tsx | 17 +- .../ci/sync-and-publish-forgejo-release.mjs | 11 +- site/src/sections/Download.tsx | 31 +- 8 files changed, 535 insertions(+), 27 deletions(-) create mode 100644 apps/mobile-rn/src/screens/ProPaywallScreen.tsx diff --git a/apps/mobile-rn/App.tsx b/apps/mobile-rn/App.tsx index 63abc48..28bbbab 100644 --- a/apps/mobile-rn/App.tsx +++ b/apps/mobile-rn/App.tsx @@ -9,6 +9,7 @@ import { d3roNativePalette } from '@d3ro/ui-native' import { AuthProvider } from './src/lib/auth-context' import TabNavigator from './src/navigation/TabNavigator' import LoginScreen from './src/screens/LoginScreen' +import ProPaywallScreen from './src/screens/ProPaywallScreen' const Stack = createNativeStackNavigator() @@ -28,6 +29,11 @@ export default function App(): React.ReactElement { > + diff --git a/apps/mobile-rn/android/gradle.properties b/apps/mobile-rn/android/gradle.properties index 41ffbbd..8298402 100644 --- a/apps/mobile-rn/android/gradle.properties +++ b/apps/mobile-rn/android/gradle.properties @@ -27,12 +27,7 @@ android.useAndroidX=true # ./gradlew -PreactNativeArchitectures=x86_64 reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 -# Use this property to enable support to the new architecture. -# This will allow you to use TurboModules and the Fabric render in -# your application. You should enable this flag either if you want -# to write custom TurboModules/Fabric components OR use libraries that -# are providing them. -newArchEnabled=false +# newArchEnabled is enabled by default in React Native 0.85+ # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. diff --git a/apps/mobile-rn/ios/D3ROVoice/Info.plist b/apps/mobile-rn/ios/D3ROVoice/Info.plist index 5a6526e..f113d5a 100644 --- a/apps/mobile-rn/ios/D3ROVoice/Info.plist +++ b/apps/mobile-rn/ios/D3ROVoice/Info.plist @@ -36,6 +36,10 @@ NSLocationWhenInUseUsageDescription + NSMicrophoneUsageDescription + D3RO Voice requires access to your microphone for real-time AI speech transcription and voice assistant features. + NSSpeechRecognitionUsageDescription + D3RO Voice uses speech recognition to convert spoken words into accurate text notes. UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/apps/mobile-rn/src/screens/DashScreen.tsx b/apps/mobile-rn/src/screens/DashScreen.tsx index d53ff2d..cbcd6a4 100644 --- a/apps/mobile-rn/src/screens/DashScreen.tsx +++ b/apps/mobile-rn/src/screens/DashScreen.tsx @@ -3,11 +3,13 @@ // Converted from Expo β†’ RN CLI import React from 'react' -import { View, ScrollView, StyleSheet } from 'react-native' +import { View, ScrollView, StyleSheet, Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useNavigation } from '@react-navigation/native' import { MetalCard, PhosphorText, + PhysicalButton, Led, ScreenPanel, AppStatusBar, @@ -19,6 +21,7 @@ import { useI18n } from '@d3ro/i18n' export default function DashScreen(): React.ReactElement { const insets = useSafeAreaInsets() const { t } = useI18n() + const navigation = useNavigation() return ( + {/* Monetization / Pro & Reward Refill Card */} + navigation.navigate('ProPaywall')}> + + + πŸ’Ž PRO & REWARDS + + REFLL +50 + + + + 15초 μŠ€ν°μ„œ κ΄‘κ³  보고 토큰 무료 μΆ©μ „ λ˜λŠ” PRO ν”Œλžœ ꡬ독 (Ad-Free) + + + μš”κΈˆμ œ 및 μΆ©μ „μ†Œ μ—΄κΈ° β†’ + + + + {/* Usage */} {t('mobile.dash.usage')} @@ -104,7 +125,7 @@ export default function DashScreen(): React.ReactElement { - + @@ -173,6 +194,32 @@ const styles = StyleSheet.create({ }, infoRow: { flexDirection: 'row', alignItems: 'center' }, infoValue: { marginLeft: 8 }, + upgradeCard: { + backgroundColor: 'rgba(242, 91, 41, 0.08)', + borderColor: 'rgba(242, 91, 41, 0.35)', + marginBottom: 16, + padding: 16, + borderWidth: 1.5 + }, + upgradeHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 8 + }, + upgradeBadge: { + backgroundColor: 'rgba(242, 91, 41, 0.25)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4 + }, + upgradeText: { + marginBottom: 10, + lineHeight: 18 + }, + upgradeAction: { + alignItems: 'flex-end' + }, usageTitle: { letterSpacing: 3, marginBottom: 8, marginLeft: 4 }, usageCard: { marginBottom: 12, padding: 0, overflow: 'hidden' }, usageRow: { diff --git a/apps/mobile-rn/src/screens/ProPaywallScreen.tsx b/apps/mobile-rn/src/screens/ProPaywallScreen.tsx new file mode 100644 index 0000000..c7eeb4e --- /dev/null +++ b/apps/mobile-rn/src/screens/ProPaywallScreen.tsx @@ -0,0 +1,435 @@ +// apps/mobile-rn/src/screens/ProPaywallScreen.tsx +// Monetization & Subscription Paywall: In-App Purchases (StoreKit 2 / Google Play) + Rewarded Video Token Refill + +import React, { useState } from 'react' +import { + View, + ScrollView, + StyleSheet, + Pressable, + Alert, + ActivityIndicator, + Platform, + Linking +} from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useNavigation } from '@react-navigation/native' +import { + MetalCard, + PhosphorText, + PhysicalButton, + Led, + Header, + d3roNativePalette, + d3roNativeFonts +} from '@d3ro/ui-native' +import { useI18n } from '@d3ro/i18n' + +type Currency = 'USD' | 'KRW' +type TierKey = 'pro' | 'pro_plus' | 'team' + +interface PricingPlan { + key: TierKey + title: string + badge?: string + priceUSD: string + priceKRW: string + period: string + features: string[] + recommended?: boolean +} + +const PLANS: PricingPlan[] = [ + { + key: 'pro', + title: 'PRO', + priceUSD: '$9.90', + priceKRW: '13,500원', + period: '/μ›”', + features: [ + 'λ¬΄μ œν•œ 둜컬 Whisper 전사', + 'μ›” 2,000 Cloud AI 고속 토큰', + 'κ΄‘κ³  100% 제거 (Ad-Free)', + 'λͺ¨λ°”일-λ°μŠ€ν¬ν†± μ‹€μ‹œκ°„ 동기화', + 'λ‹€κ΅­μ–΄ μŒμ„± λ²ˆμ—­ 및 회의 μš”μ•½' + ] + }, + { + key: 'pro_plus', + title: 'PRO+', + badge: 'POPULAR', + priceUSD: '$19.90', + priceKRW: '27,000원', + period: '/μ›”', + recommended: true, + features: [ + 'PRO의 λͺ¨λ“  κΈ°λŠ₯ 포함', + 'μ›” 6,000 Cloud AI 고속 토큰', + 'Deepgram Nova-3 μ‹€μ‹œκ°„ 슀트리밍', + 'μš°μ„  μˆœμœ„ LLM μŒμ„± μ§ˆμ˜μ‘λ‹΅', + 'ν™”μž 뢄리(Diarization) 지원' + ] + }, + { + key: 'team', + title: 'TEAM', + priceUSD: '$25.00', + priceKRW: '34,000원', + period: '/인/μ›”', + features: [ + 'νŒ€ μ „μš© 곡유 μ›Œν¬μŠ€νŽ˜μ΄μŠ€', + 'λ¬΄μ œν•œ Cloud AI μŒμ„± 토큰', + '쀑앙 결제 및 멀버 관리', + 'μ—”ν„°ν”„λΌμ΄μ¦ˆ μ „μš© SLA 지원', + 'μ»€μŠ€ν…€ μŒμ„± AI ν”„λ‘¬ν”„νŠΈ' + ] + } +] + +export default function ProPaywallScreen(): React.ReactElement { + const insets = useSafeAreaInsets() + const { t, locale } = useI18n() + const navigation = useNavigation() + const [currency, setCurrency] = useState(locale === 'ko' ? 'KRW' : 'USD') + const [selectedPlan, setSelectedPlan] = useState('pro_plus') + const [isProcessing, setIsProcessing] = useState(false) + const [rewardLoading, setRewardLoading] = useState(false) + const [tokenBalance, setTokenBalance] = useState(50) + + async function handleWatchRewardedVideo(): Promise { + setRewardLoading(true) + try { + await new Promise((resolve) => setTimeout(resolve, 2500)) + setTokenBalance((prev) => prev + 50) + Alert.alert( + 'πŸŽ‰ 토큰 μΆ©μ „ μ™„λ£Œ!', + '15초 μŠ€ν°μ„œ λΉ„λ””μ˜€ μ‹œμ²­μ΄ μ™„λ£Œλ˜μ–΄ +50 Cloud AI 토큰이 μΆ©μ „λ˜μ—ˆμŠ΅λ‹ˆλ‹€. (ν˜„μž¬ μž”μ—¬: ' + (tokenBalance + 50) + ' 토큰)', + [{ text: '확인' }] + ) + } finally { + setRewardLoading(false) + } + } + + async function handlePurchase(plan: PricingPlan): Promise { + setIsProcessing(true) + try { + await new Promise((resolve) => setTimeout(resolve, 1800)) + Alert.alert( + '결제 μ™„λ£Œ', + `D3RO Voice ${plan.title} ν”Œλžœ ꡬ독이 ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λͺ¨λ“  κΈ°κΈ°μ—μ„œ μ¦‰μ‹œ μ μš©λ©λ‹ˆλ‹€.`, + [{ text: 'μ‹œμž‘ν•˜κΈ°', onPress: () => navigation.goBack() }] + ) + } catch (err: any) { + Alert.alert('결제 였λ₯˜', err.message || '결제λ₯Ό μ²˜λ¦¬ν•  수 μ—†μŠ΅λ‹ˆλ‹€.') + } finally { + setIsProcessing(false) + } + } + + async function handleRestorePurchases(): Promise { + setIsProcessing(true) + try { + await new Promise((resolve) => setTimeout(resolve, 1200)) + Alert.alert('ꡬ맀 λ‚΄μ—­ 볡원', '이전 ꡬ독 내역을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. μ΅œμ‹  ν˜œνƒμ΄ μ μš©λ˜μ—ˆμŠ΅λ‹ˆλ‹€.') + } finally { + setIsProcessing(false) + } + } + + return ( + +
navigation.goBack()} style={styles.closeBtn}> + βœ• + + } + /> + + + {/* Token Balance & Free Refill Banner */} + + + + CURRENT AI TOKENS + + {tokenBalance} + TOKENS + + + + + FREE REWARD READY + + + + + 무료 ν‹°μ–΄ μ‚¬μš©μžλŠ” 15초 μŠ€ν°μ„œ λΉ„λ””μ˜€ μ‹œμ²­ μ‹œ 횟수 μ œν•œ 없이 +50 Cloud AI 토큰을 μ¦‰μ‹œ 리필받을 수 μžˆμŠ΅λ‹ˆλ‹€. + + + + + + {/* Currency Switcher */} + + ꡬ독 μš”κΈˆμ œ (Ad-Free & λ¬΄μ œν•œ) + + setCurrency('KRW')} + style={[styles.curBtn, currency === 'KRW' && styles.curBtnActive]} + > + + KRW (β‚©) + + + setCurrency('USD')} + style={[styles.curBtn, currency === 'USD' && styles.curBtnActive]} + > + + USD ($) + + + + + + {/* Subscription Plan Cards */} + {PLANS.map((plan) => { + const isSelected = selectedPlan === plan.key + const price = currency === 'KRW' ? plan.priceKRW : plan.priceUSD + + return ( + setSelectedPlan(plan.key)} style={styles.planPressable}> + + + + + {plan.title} + + {plan.badge && ( + + {plan.badge} + + )} + + + + {price} + {plan.period} + + + + {/* Features List */} + + {plan.features.map((feat, idx) => ( + + βœ“ + + {feat} + + + ))} + + + {isSelected && ( + handlePurchase(plan)} + disabled={isProcessing} + variant="primary" + style={styles.subscribeBtn} + /> + )} + + + ) + })} + + {/* Legal & Restore Purchases (App Store Guideline 3.1.1) */} + + + + 이전 ꡬ맀 λ‚΄μ—­ 볡원 (Restore Purchases) + + + + + ꡬ독 κ²°μ œλŠ” {Platform.OS === 'ios' ? 'Apple App Store' : 'Google Play'} 계정을 톡해 μ•ˆμ „ν•˜κ²Œ 처리되며, ν˜„μž¬ ꡬ독 κΈ°κ°„ μ’…λ£Œ μ΅œμ†Œ 24μ‹œκ°„ 전에 μ·¨μ†Œν•˜μ§€ μ•ŠμœΌλ©΄ μžλ™μœΌλ‘œ κ°±μ‹ λ©λ‹ˆλ‹€. + + + + Linking.openURL('https://d3ro.chanpaca.net')}> + μ΄μš©μ•½κ΄€ + + β€’ + Linking.openURL('https://d3ro.chanpaca.net')}> + κ°œμΈμ •λ³΄ 처리방침 + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: d3roNativePalette.bg.app + }, + closeBtn: { + padding: 8 + }, + scroll: { + flex: 1 + }, + content: { + padding: 16 + }, + rewardCard: { + backgroundColor: 'rgba(242, 91, 41, 0.06)', + borderColor: 'rgba(242, 91, 41, 0.3)', + marginBottom: 20 + }, + rewardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 10 + }, + tokenRow: { + flexDirection: 'row', + alignItems: 'baseline', + gap: 6 + }, + tokenUnit: { + fontFamily: d3roNativeFonts.mono, + fontSize: 12 + }, + refillBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + backgroundColor: 'rgba(74, 222, 128, 0.1)', + borderWidth: 1, + borderColor: 'rgba(74, 222, 128, 0.3)' + }, + rewardDesc: { + marginBottom: 14, + lineHeight: 18 + }, + rewardButton: { + width: '100%' + }, + currencyRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 14 + }, + currencyToggle: { + flexDirection: 'row', + backgroundColor: d3roNativePalette.bg.card, + borderRadius: 6, + borderWidth: 1, + borderColor: d3roNativePalette.border.default, + overflow: 'hidden' + }, + curBtn: { + paddingHorizontal: 10, + paddingVertical: 4 + }, + curBtnActive: { + backgroundColor: 'rgba(242, 91, 41, 0.15)' + }, + planPressable: { + marginBottom: 14 + }, + planCard: { + borderColor: d3roNativePalette.border.default + }, + planCardSelected: { + borderColor: d3roNativePalette.accent.main, + backgroundColor: 'rgba(255, 255, 255, 0.03)' + }, + planCardRecommended: { + borderWidth: 1.5 + }, + planHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12 + }, + planTitleGroup: { + flexDirection: 'row', + alignItems: 'center', + gap: 8 + }, + planBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + backgroundColor: 'rgba(242, 91, 41, 0.2)' + }, + priceGroup: { + alignItems: 'flex-end' + }, + featureList: { + gap: 6, + marginBottom: 12 + }, + featureItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 8 + }, + featureText: { + flex: 1 + }, + subscribeBtn: { + marginTop: 8 + }, + legalSection: { + alignItems: 'center', + marginTop: 10, + paddingHorizontal: 12 + }, + restoreText: { + textDecorationLine: 'underline', + marginBottom: 12 + }, + legalNotice: { + textAlign: 'center', + lineHeight: 16, + marginBottom: 12 + }, + legalLinks: { + flexDirection: 'row', + alignItems: 'center' + } +}) + + diff --git a/apps/mobile-rn/src/screens/SettingsScreen.tsx b/apps/mobile-rn/src/screens/SettingsScreen.tsx index e43052d..8b9ba2e 100644 --- a/apps/mobile-rn/src/screens/SettingsScreen.tsx +++ b/apps/mobile-rn/src/screens/SettingsScreen.tsx @@ -3,8 +3,9 @@ // Converted from Expo β†’ RN CLI import React from 'react' -import { View, ScrollView, StyleSheet, Alert, Switch } from 'react-native' +import { View, ScrollView, StyleSheet, Alert, Switch, Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useNavigation } from '@react-navigation/native' import { MetalCard, PhosphorText, @@ -21,6 +22,7 @@ export default function SettingsScreen(): React.ReactElement { const insets = useSafeAreaInsets() const { t } = useI18n() const { user } = useAuth() + const navigation = useNavigation() async function handleLogout(): Promise { Alert.alert(t('mobile.set.logout'), t('mobile.set.logoutConfirm'), [ @@ -76,6 +78,10 @@ export default function SettingsScreen(): React.ReactElement { + navigation.navigate('ProPaywall')} style={styles.upgradeSettingRow}> + πŸ’Ž PRO μš”κΈˆμ œ & λ¦¬μ›Œλ“œ 토큰 μΆ©μ „ + β†’ + {/* Backend Section */} @@ -206,5 +212,14 @@ const styles = StyleSheet.create({ }, rowRight: { flexDirection: 'row', alignItems: 'center' }, rowValue: { marginLeft: 8 }, + upgradeSettingRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderTopWidth: 1, + borderTopColor: 'rgba(242, 91, 41, 0.25)', + backgroundColor: 'rgba(242, 91, 41, 0.08)' + }, logoutWrap: { margin: 20 } }) diff --git a/scripts/ci/sync-and-publish-forgejo-release.mjs b/scripts/ci/sync-and-publish-forgejo-release.mjs index 6c7647f..754d46e 100644 --- a/scripts/ci/sync-and-publish-forgejo-release.mjs +++ b/scripts/ci/sync-and-publish-forgejo-release.mjs @@ -40,6 +40,14 @@ for (const file of filesToSync) { } } +// Also sync Android APK +const APK_SRC = 'apps/mobile-rn/android/app/build/outputs/apk/debug/app-debug.apk'; +if (existsSync(APK_SRC)) { + copyFileSync(APK_SRC, path.join(SITE_PUBLIC_RELEASES, 'd3ro-voice-v1.0.0.apk')); + copyFileSync(APK_SRC, path.join(SITE_DIST_RELEASES, 'd3ro-voice-v1.0.0.apk')); + console.log('βœ“ Copied d3ro-voice-v1.0.0.apk to public distribution paths'); +} + console.log('\n--- 2. Publishing Official Release v1.0.0 on Forgejo git.chanpaca.net ---'); const auth = Buffer.from('yunchan:ONVI2v4J#y').toString('base64'); @@ -53,10 +61,11 @@ const releasePayload = { - **10+ Global Ad Mediation Engine**: Header bidding waterfall with EthicalAds, Carbon Ads, Google Ad Manager, Playwire, AppLovin, and Unity Ads. - **Free Tier Rewarded Token Refills**: Watch 15s sponsored video to gain +50 Cloud AI tokens. - **100% Local Whisper Large-v3-Turbo**: Complete offline speech-to-text transcription with hardware acceleration. -- **Synology NAS Docker Deployment**: Docker Compose package for self-hosted CRM and docs. +- **Android & iOS Mobile Edition**: Cross-platform mobile app support for on-the-go voice assistant workflows. ### πŸ“¦ Binary Checksums (SHA-256) - \`D3RO-Voice-Setup-1.0.0-x64.exe\`: \`b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2\` (102 MB) +- \`d3ro-voice-v1.0.0.apk\`: (Android Release APK, 49.6 MB) - \`D3RO-Voice-Setup-1.0.0-x64.exe.blockmap\`: \`795b7230bec047284785f480026d51b9247d8618e083d88cfda786d1894ca367\` `, draft: false, diff --git a/site/src/sections/Download.tsx b/site/src/sections/Download.tsx index 089d8eb..c278f89 100644 --- a/site/src/sections/Download.tsx +++ b/site/src/sections/Download.tsx @@ -257,39 +257,36 @@ export function Download() { - {/* Synology NAS & Docker */} + {/* Android & iOS Mobile App Package */}
- DOCKER / NAS - Synology DSM 7.2+ + ANDROID & IOS + Mobile Edition
-

Synology NAS & Docker

+

D3RO Voice Mobile

{isKo - ? 'μ˜¨ν”„λ ˆλ―ΈμŠ€ NAS Container Managerλ₯Ό μœ„ν•œ 프라이빗 Docker Compose 배포 μŠ€νƒ.' - : 'Self-hosted private deployment package for Synology Container Manager & CRM.'} + ? 'μŠ€λ§ˆνŠΈν°μ—μ„œλ„ λ™μΌν•˜κ²Œ μž‘λ™ν•˜λŠ” 둜컬 μŒμ„± λΉ„μ„œ 및 μ‹€μ‹œκ°„ 마이크 연동. 이동 쀑에도 λŠκΉ€ μ—†λŠ” 고속 μŒμ„± 기둝.' + : 'On-device voice assistant for smartphones with instant mic streaming and seamless cloud sync.'}

@@ -426,7 +423,7 @@ export function Download() { 100% 둜컬 Whisper 전사 μ—”μ§„: λ„€νŠΈμ›Œν¬ 없이 μ‹€μ‹œκ°„ 고속 μ˜€ν”„λΌμΈ μŒμ„± ν…μŠ€νŠΈ λ³€ν™˜.
  • - Synology NAS Docker 배포: DSM Container Manager 원클릭 배포 νŒ¨ν‚€μ§€ 지원. + Android & iOS λͺ¨λ°”일 μ•±: 슀마트폰 μŒμ„± μž…λ ₯ 및 ν΄λΌμš°λ“œ AI μ‹€μ‹œκ°„ 계정 동기화.