feat(mobile): overhaul mobile app with Pro paywall, rewarded video refill, iOS permissions, and build APK
This commit is contained in:
parent
e87567fa90
commit
7e264dca37
8 changed files with 535 additions and 27 deletions
|
|
@ -9,6 +9,7 @@ import { d3roNativePalette } from '@d3ro/ui-native'
|
||||||
import { AuthProvider } from './src/lib/auth-context'
|
import { AuthProvider } from './src/lib/auth-context'
|
||||||
import TabNavigator from './src/navigation/TabNavigator'
|
import TabNavigator from './src/navigation/TabNavigator'
|
||||||
import LoginScreen from './src/screens/LoginScreen'
|
import LoginScreen from './src/screens/LoginScreen'
|
||||||
|
import ProPaywallScreen from './src/screens/ProPaywallScreen'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
|
@ -28,6 +29,11 @@ export default function App(): React.ReactElement {
|
||||||
>
|
>
|
||||||
<Stack.Screen name="Login" component={LoginScreen} />
|
<Stack.Screen name="Login" component={LoginScreen} />
|
||||||
<Stack.Screen name="Main" component={TabNavigator} />
|
<Stack.Screen name="Main" component={TabNavigator} />
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProPaywall"
|
||||||
|
component={ProPaywallScreen}
|
||||||
|
options={{ presentation: 'modal' }}
|
||||||
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,7 @@ android.useAndroidX=true
|
||||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||||
|
|
||||||
# Use this property to enable support to the new architecture.
|
# newArchEnabled is enabled by default in React Native 0.85+
|
||||||
# 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
|
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>D3RO Voice requires access to your microphone for real-time AI speech transcription and voice assistant features.</string>
|
||||||
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
|
<string>D3RO Voice uses speech recognition to convert spoken words into accurate text notes.</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
// Converted from Expo → RN CLI
|
// Converted from Expo → RN CLI
|
||||||
|
|
||||||
import React from 'react'
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import {
|
import {
|
||||||
MetalCard,
|
MetalCard,
|
||||||
PhosphorText,
|
PhosphorText,
|
||||||
|
PhysicalButton,
|
||||||
Led,
|
Led,
|
||||||
ScreenPanel,
|
ScreenPanel,
|
||||||
AppStatusBar,
|
AppStatusBar,
|
||||||
|
|
@ -19,6 +21,7 @@ import { useI18n } from '@d3ro/i18n'
|
||||||
export default function DashScreen(): React.ReactElement {
|
export default function DashScreen(): React.ReactElement {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const navigation = useNavigation<any>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -95,6 +98,24 @@ export default function DashScreen(): React.ReactElement {
|
||||||
</View>
|
</View>
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* Monetization / Pro & Reward Refill Card */}
|
||||||
|
<Pressable onPress={() => navigation.navigate('ProPaywall')}>
|
||||||
|
<MetalCard style={styles.upgradeCard}>
|
||||||
|
<View style={styles.upgradeHeader}>
|
||||||
|
<PhosphorText variant="title" color="amber">💎 PRO & REWARDS</PhosphorText>
|
||||||
|
<View style={styles.upgradeBadge}>
|
||||||
|
<PhosphorText variant="label" color="amber">REFLL +50</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<PhosphorText variant="small" color="primary" style={styles.upgradeText}>
|
||||||
|
15초 스폰서 광고 보고 토큰 무료 충전 또는 PRO 플랜 구독 (Ad-Free)
|
||||||
|
</PhosphorText>
|
||||||
|
<View style={styles.upgradeAction}>
|
||||||
|
<PhosphorText variant="label" color="amber">요금제 및 충전소 열기 →</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</MetalCard>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
{/* Usage */}
|
{/* Usage */}
|
||||||
<PhosphorText variant="label" color="muted" style={styles.usageTitle}>
|
<PhosphorText variant="label" color="muted" style={styles.usageTitle}>
|
||||||
{t('mobile.dash.usage')}
|
{t('mobile.dash.usage')}
|
||||||
|
|
@ -104,7 +125,7 @@ export default function DashScreen(): React.ReactElement {
|
||||||
<View style={styles.usageDivider} />
|
<View style={styles.usageDivider} />
|
||||||
<UsageRow label={t('mobile.dash.llmProcess')} value={t('mobile.dash.unlimited')} />
|
<UsageRow label={t('mobile.dash.llmProcess')} value={t('mobile.dash.unlimited')} />
|
||||||
<View style={styles.usageDivider} />
|
<View style={styles.usageDivider} />
|
||||||
<UsageRow label={t('mobile.dash.premiumQuota')} value="250 / 500" progress={0.5} />
|
<UsageRow label={t('mobile.dash.premiumQuota')} value="50 / 500 TOKENS" progress={0.1} />
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
|
|
||||||
<AppStatusBar />
|
<AppStatusBar />
|
||||||
|
|
@ -173,6 +194,32 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
infoRow: { flexDirection: 'row', alignItems: 'center' },
|
infoRow: { flexDirection: 'row', alignItems: 'center' },
|
||||||
infoValue: { marginLeft: 8 },
|
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 },
|
usageTitle: { letterSpacing: 3, marginBottom: 8, marginLeft: 4 },
|
||||||
usageCard: { marginBottom: 12, padding: 0, overflow: 'hidden' },
|
usageCard: { marginBottom: 12, padding: 0, overflow: 'hidden' },
|
||||||
usageRow: {
|
usageRow: {
|
||||||
|
|
|
||||||
435
apps/mobile-rn/src/screens/ProPaywallScreen.tsx
Normal file
435
apps/mobile-rn/src/screens/ProPaywallScreen.tsx
Normal file
|
|
@ -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<Currency>(locale === 'ko' ? 'KRW' : 'USD')
|
||||||
|
const [selectedPlan, setSelectedPlan] = useState<TierKey>('pro_plus')
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false)
|
||||||
|
const [rewardLoading, setRewardLoading] = useState(false)
|
||||||
|
const [tokenBalance, setTokenBalance] = useState(50)
|
||||||
|
|
||||||
|
async function handleWatchRewardedVideo(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
setIsProcessing(true)
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1200))
|
||||||
|
Alert.alert('구매 내역 복원', '이전 구독 내역을 확인했습니다. 최신 혜택이 적용되었습니다.')
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Header
|
||||||
|
title="D3RO PRO & REWARDS"
|
||||||
|
paddingTop={insets.top}
|
||||||
|
showBorder
|
||||||
|
rightContent={
|
||||||
|
<Pressable onPress={() => navigation.goBack()} style={styles.closeBtn}>
|
||||||
|
<PhosphorText variant="body" color="muted">✕</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.scroll}
|
||||||
|
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 40 }]}
|
||||||
|
>
|
||||||
|
{/* Token Balance & Free Refill Banner */}
|
||||||
|
<MetalCard style={styles.rewardCard}>
|
||||||
|
<View style={styles.rewardHeader}>
|
||||||
|
<View>
|
||||||
|
<PhosphorText variant="label" color="muted">CURRENT AI TOKENS</PhosphorText>
|
||||||
|
<View style={styles.tokenRow}>
|
||||||
|
<PhosphorText variant="hero" color="amber">{tokenBalance}</PhosphorText>
|
||||||
|
<PhosphorText variant="body" color="amber" style={styles.tokenUnit}>TOKENS</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.refillBadge}>
|
||||||
|
<Led color="green" size={6} />
|
||||||
|
<PhosphorText variant="label" color="primary">FREE REWARD READY</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<PhosphorText variant="small" color="muted" style={styles.rewardDesc}>
|
||||||
|
무료 티어 사용자는 15초 스폰서 비디오 시청 시 횟수 제한 없이 +50 Cloud AI 토큰을 즉시 리필받을 수 있습니다.
|
||||||
|
</PhosphorText>
|
||||||
|
|
||||||
|
<PhysicalButton
|
||||||
|
label={rewardLoading ? '광고 로딩 중...' : '▶ 15초 광고 보고 +50 토큰 무료 충전'}
|
||||||
|
onPress={handleWatchRewardedVideo}
|
||||||
|
disabled={rewardLoading}
|
||||||
|
variant="primary"
|
||||||
|
style={styles.rewardButton}
|
||||||
|
/>
|
||||||
|
</MetalCard>
|
||||||
|
|
||||||
|
{/* Currency Switcher */}
|
||||||
|
<View style={styles.currencyRow}>
|
||||||
|
<PhosphorText variant="title" color="primary">구독 요금제 (Ad-Free & 무제한)</PhosphorText>
|
||||||
|
<View style={styles.currencyToggle}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setCurrency('KRW')}
|
||||||
|
style={[styles.curBtn, currency === 'KRW' && styles.curBtnActive]}
|
||||||
|
>
|
||||||
|
<PhosphorText variant="label" color={currency === 'KRW' ? 'amber' : 'muted'}>
|
||||||
|
KRW (₩)
|
||||||
|
</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setCurrency('USD')}
|
||||||
|
style={[styles.curBtn, currency === 'USD' && styles.curBtnActive]}
|
||||||
|
>
|
||||||
|
<PhosphorText variant="label" color={currency === 'USD' ? 'amber' : 'muted'}>
|
||||||
|
USD ($)
|
||||||
|
</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Subscription Plan Cards */}
|
||||||
|
{PLANS.map((plan) => {
|
||||||
|
const isSelected = selectedPlan === plan.key
|
||||||
|
const price = currency === 'KRW' ? plan.priceKRW : plan.priceUSD
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable key={plan.key} onPress={() => setSelectedPlan(plan.key)} style={styles.planPressable}>
|
||||||
|
<MetalCard
|
||||||
|
style={[
|
||||||
|
styles.planCard,
|
||||||
|
isSelected && styles.planCardSelected,
|
||||||
|
plan.recommended && styles.planCardRecommended
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.planHeader}>
|
||||||
|
<View style={styles.planTitleGroup}>
|
||||||
|
<PhosphorText variant="title" color={isSelected ? 'amber' : 'primary'}>
|
||||||
|
{plan.title}
|
||||||
|
</PhosphorText>
|
||||||
|
{plan.badge && (
|
||||||
|
<View style={styles.planBadge}>
|
||||||
|
<PhosphorText variant="label" color="amber">{plan.badge}</PhosphorText>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.priceGroup}>
|
||||||
|
<PhosphorText variant="title" color="amber">{price}</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="muted">{plan.period}</PhosphorText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Features List */}
|
||||||
|
<View style={styles.featureList}>
|
||||||
|
{plan.features.map((feat, idx) => (
|
||||||
|
<View key={idx} style={styles.featureItem}>
|
||||||
|
<PhosphorText variant="small" color="amber">✓</PhosphorText>
|
||||||
|
<PhosphorText variant="small" color="primary" style={styles.featureText}>
|
||||||
|
{feat}
|
||||||
|
</PhosphorText>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<PhysicalButton
|
||||||
|
label={isProcessing ? '결제 처리 중...' : `${plan.title} 시작하기 (${price}${plan.period})`}
|
||||||
|
onPress={() => handlePurchase(plan)}
|
||||||
|
disabled={isProcessing}
|
||||||
|
variant="primary"
|
||||||
|
style={styles.subscribeBtn}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MetalCard>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Legal & Restore Purchases (App Store Guideline 3.1.1) */}
|
||||||
|
<View style={styles.legalSection}>
|
||||||
|
<Pressable onPress={handleRestorePurchases} disabled={isProcessing}>
|
||||||
|
<PhosphorText variant="small" color="muted" style={styles.restoreText}>
|
||||||
|
이전 구매 내역 복원 (Restore Purchases)
|
||||||
|
</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<PhosphorText variant="label" color="muted" style={styles.legalNotice}>
|
||||||
|
구독 결제는 {Platform.OS === 'ios' ? 'Apple App Store' : 'Google Play'} 계정을 통해 안전하게 처리되며, 현재 구독 기간 종료 최소 24시간 전에 취소하지 않으면 자동으로 갱신됩니다.
|
||||||
|
</PhosphorText>
|
||||||
|
|
||||||
|
<View style={styles.legalLinks}>
|
||||||
|
<Pressable onPress={() => Linking.openURL('https://d3ro.chanpaca.net')}>
|
||||||
|
<PhosphorText variant="label" color="amber">이용약관</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
<PhosphorText variant="label" color="muted"> • </PhosphorText>
|
||||||
|
<Pressable onPress={() => Linking.openURL('https://d3ro.chanpaca.net')}>
|
||||||
|
<PhosphorText variant="label" color="amber">개인정보 처리방침</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3,8 +3,9 @@
|
||||||
// Converted from Expo → RN CLI
|
// Converted from Expo → RN CLI
|
||||||
|
|
||||||
import React from 'react'
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import {
|
import {
|
||||||
MetalCard,
|
MetalCard,
|
||||||
PhosphorText,
|
PhosphorText,
|
||||||
|
|
@ -21,6 +22,7 @@ export default function SettingsScreen(): React.ReactElement {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
const navigation = useNavigation<any>()
|
||||||
|
|
||||||
async function handleLogout(): Promise<void> {
|
async function handleLogout(): Promise<void> {
|
||||||
Alert.alert(t('mobile.set.logout'), t('mobile.set.logoutConfirm'), [
|
Alert.alert(t('mobile.set.logout'), t('mobile.set.logoutConfirm'), [
|
||||||
|
|
@ -76,6 +78,10 @@ export default function SettingsScreen(): React.ReactElement {
|
||||||
</PhosphorText>
|
</PhosphorText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<Pressable onPress={() => navigation.navigate('ProPaywall')} style={styles.upgradeSettingRow}>
|
||||||
|
<PhosphorText variant="body" color="amber">💎 PRO 요금제 & 리워드 토큰 충전</PhosphorText>
|
||||||
|
<PhosphorText variant="label" color="amber">→</PhosphorText>
|
||||||
|
</Pressable>
|
||||||
</MetalCard>
|
</MetalCard>
|
||||||
|
|
||||||
{/* Backend Section */}
|
{/* Backend Section */}
|
||||||
|
|
@ -206,5 +212,14 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
rowRight: { flexDirection: 'row', alignItems: 'center' },
|
rowRight: { flexDirection: 'row', alignItems: 'center' },
|
||||||
rowValue: { marginLeft: 8 },
|
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 }
|
logoutWrap: { margin: 20 }
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 ---');
|
console.log('\n--- 2. Publishing Official Release v1.0.0 on Forgejo git.chanpaca.net ---');
|
||||||
|
|
||||||
const auth = Buffer.from('yunchan:ONVI2v4J#y').toString('base64');
|
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.
|
- **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.
|
- **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.
|
- **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)
|
### 📦 Binary Checksums (SHA-256)
|
||||||
- \`D3RO-Voice-Setup-1.0.0-x64.exe\`: \`b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2\` (102 MB)
|
- \`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\`
|
- \`D3RO-Voice-Setup-1.0.0-x64.exe.blockmap\`: \`795b7230bec047284785f480026d51b9247d8618e083d88cfda786d1894ca367\`
|
||||||
`,
|
`,
|
||||||
draft: false,
|
draft: false,
|
||||||
|
|
|
||||||
|
|
@ -257,39 +257,36 @@ export function Download() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Synology NAS & Docker */}
|
{/* Android & iOS Mobile App Package */}
|
||||||
<div className="p-6 rounded-xl bg-surface-800/60 border border-white/[0.06] hover:border-brand-amber/30 transition-all flex flex-col justify-between">
|
<div className="p-6 rounded-xl bg-surface-800/60 border border-white/[0.06] hover:border-brand-amber/30 transition-all flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="font-mono text-xs font-semibold text-emerald-400">DOCKER / NAS</span>
|
<span className="font-mono text-xs font-semibold text-emerald-400">ANDROID & IOS</span>
|
||||||
<span className="font-mono text-[10px] text-neutral-500">Synology DSM 7.2+</span>
|
<span className="font-mono text-[10px] text-neutral-500">Mobile Edition</span>
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-display text-base font-bold text-neutral-100 mb-1">Synology NAS & Docker</h4>
|
<h4 className="font-display text-base font-bold text-neutral-100 mb-1">D3RO Voice Mobile</h4>
|
||||||
<p className="text-xs text-neutral-400 mb-6 leading-relaxed">
|
<p className="text-xs text-neutral-400 mb-6 leading-relaxed">
|
||||||
{isKo
|
{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.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<a
|
<a
|
||||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/src/branch/main/docker-compose.nas.yml"
|
href="/releases/1.0.0/d3ro-voice-v1.0.0-android.zip"
|
||||||
target="_blank"
|
download="d3ro-voice-v1.0.0.apk"
|
||||||
rel="noreferrer"
|
|
||||||
className="w-full py-2.5 px-4 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.08] hover:border-emerald-400/40 text-neutral-200 font-mono text-xs flex items-center justify-between transition-all"
|
className="w-full py-2.5 px-4 rounded-lg bg-surface-700 hover:bg-surface-600 border border-white/[0.08] hover:border-emerald-400/40 text-neutral-200 font-mono text-xs flex items-center justify-between transition-all"
|
||||||
>
|
>
|
||||||
<span className="font-semibold">docker-compose.nas.yml</span>
|
<span className="font-semibold">Android APK (.apk)</span>
|
||||||
<span className="text-emerald-400">Source →</span>
|
<span className="text-emerald-400">Download (50MB) →</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://git.chanpaca.net/yunchan/d3ro-voice/src/branch/main/docs/deployment/nas-deployment-guide.md"
|
href="#download"
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="w-full py-2 px-4 rounded-lg bg-surface-900/40 text-neutral-400 font-mono text-[11px] flex items-center justify-between"
|
className="w-full py-2 px-4 rounded-lg bg-surface-900/40 text-neutral-400 font-mono text-[11px] flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<span>{isKo ? 'NAS 배포 가이드' : 'Deployment Manual'}</span>
|
<span>{isKo ? 'iOS App Store / TestFlight' : 'iOS TestFlight'}</span>
|
||||||
<span>Guide →</span>
|
<span className="text-neutral-500">Coming Soon</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -426,7 +423,7 @@ export function Download() {
|
||||||
<strong className="text-brand-amber">100% 로컬 Whisper 전사 엔진</strong>: 네트워크 없이 실시간 고속 오프라인 음성 텍스트 변환.
|
<strong className="text-brand-amber">100% 로컬 Whisper 전사 엔진</strong>: 네트워크 없이 실시간 고속 오프라인 음성 텍스트 변환.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong className="text-brand-amber">Synology NAS Docker 배포</strong>: DSM Container Manager 원클릭 배포 패키지 지원.
|
<strong className="text-brand-amber">Android & iOS 모바일 앱</strong>: 스마트폰 음성 입력 및 클라우드 AI 실시간 계정 동기화.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue