@@ -25,6 +25,8 @@ import { color, font, radius, space } from '../theme/tokens';
2525
2626interface Props {
2727 readonly tier : Tier ;
28+ /** RFC3339 timestamp premium is paid through, or null when free/unknown. */
29+ readonly paidUntil : string | null ;
2830 readonly payment : PaymentIdentity | null ;
2931 /** Remote flag: when true, show the in-app pay flow; else "manage on web". */
3032 readonly inAppUpgrade : boolean ;
@@ -34,8 +36,34 @@ interface Props {
3436/** Where the prefilled pay-to-address upgrade flow lives on the web. */
3537const UPGRADE_URL = 'vpn.cumulusvpn.com' ;
3638
37- export function UpgradeScreen ( { tier, payment, inAppUpgrade, onClose } : Props ) : React . JSX . Element {
39+ const MONTHS = [ 'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' , 'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec' ] ;
40+
41+ /**
42+ * Format an RFC3339 expiry into a short date + whole days remaining. Hand-rolled
43+ * (no `Intl`/`toLocaleDateString`) so it's identical across Hermes/JSC.
44+ */
45+ export function formatExpiry ( iso : string | null ) : { date : string ; daysLeft : number } | null {
46+ if ( ! iso ) {
47+ return null ;
48+ }
49+ const t = Date . parse ( iso ) ;
50+ if ( Number . isNaN ( t ) ) {
51+ return null ;
52+ }
53+ const d = new Date ( t ) ;
54+ const daysLeft = Math . max ( 0 , Math . ceil ( ( t - Date . now ( ) ) / 86_400_000 ) ) ;
55+ return { date : `${ d . getDate ( ) } ${ MONTHS [ d . getMonth ( ) ] } ${ d . getFullYear ( ) } ` , daysLeft } ;
56+ }
57+
58+ export function UpgradeScreen ( {
59+ tier,
60+ paidUntil,
61+ payment,
62+ inAppUpgrade,
63+ onClose,
64+ } : Props ) : React . JSX . Element {
3865 const premium = tier === 'premium' ;
66+ const expiry = formatExpiry ( paidUntil ) ;
3967 return (
4068 < ScrollView
4169 style = { styles . root }
@@ -55,9 +83,23 @@ export function UpgradeScreen({ tier, payment, inAppUpgrade, onClose }: Props):
5583 < TierPill tier = { tier } />
5684 </ View >
5785 { premium ? (
58- < Text style = { styles . copy } >
59- You’re on Premium — full speed on every gateway. Nothing to do here.
60- </ Text >
86+ < >
87+ < Text style = { styles . copy } >
88+ You’re on Premium — full speed on every gateway. Pay again any time to add more time;
89+ it stacks on top of your current expiry.
90+ </ Text >
91+ { expiry ? (
92+ < View style = { styles . priceRow } >
93+ < Text style = { styles . priceLabel } > Active until</ Text >
94+ < Text style = { styles . price } >
95+ { expiry . date } { ' ' }
96+ < Text style = { styles . priceUnit } >
97+ · { expiry . daysLeft } { expiry . daysLeft === 1 ? 'day' : 'days' } left
98+ </ Text >
99+ </ Text >
100+ </ View >
101+ ) : null }
102+ </ >
61103 ) : (
62104 < >
63105 < Text style = { styles . copy } >
@@ -76,17 +118,23 @@ export function UpgradeScreen({ tier, payment, inAppUpgrade, onClose }: Props):
76118 ) }
77119 </ View >
78120
79- { premium || ! payment ? null : inAppUpgrade ? (
80- < InAppPay payment = { payment } />
121+ { ! payment ? null : inAppUpgrade ? (
122+ < InAppPay payment = { payment } premium = { premium } />
81123 ) : (
82- < ManageOnWeb payment = { payment } />
124+ < ManageOnWeb payment = { payment } premium = { premium } />
83125 ) }
84126 </ ScrollView >
85127 ) ;
86128}
87129
88130/** Full in-app pay flow: QR + wallet hand-off + prefilled details. */
89- function InAppPay ( { payment } : { readonly payment : PaymentIdentity } ) : React . JSX . Element {
131+ function InAppPay ( {
132+ payment,
133+ premium,
134+ } : {
135+ readonly payment : PaymentIdentity ;
136+ readonly premium : boolean ;
137+ } ) : React . JSX . Element {
90138 const [ walletError , setWalletError ] = useState < string | null > ( null ) ;
91139 // The QR carries the BIP21 `flux:` payload — that's what a wallet's in-app
92140 // scanner (Zelcore / SSP) parses to a prefilled send.
@@ -113,6 +161,7 @@ function InAppPay({ payment }: { readonly payment: PaymentIdentity }): React.JSX
113161
114162 return (
115163 < >
164+ < Text style = { styles . section } > { premium ? 'Add more time' : 'Pay with FLUX' } </ Text >
116165 < View style = { styles . qrWrap } >
117166 < Qr value = { qrLink } size = { 196 } />
118167 < Text style = { styles . qrCap } > Scan with Zelcore / SSP Wallet</ Text >
@@ -143,7 +192,11 @@ function InAppPay({ payment }: { readonly payment: PaymentIdentity }): React.JSX
143192 />
144193 < Step
145194 n = { 3 }
146- text = "This device unlocks automatically within ~1 minute, on every gateway at once."
195+ text = {
196+ premium
197+ ? 'Another 30 days is added on top of your current expiry within ~1 minute, on every gateway at once.'
198+ : 'This device unlocks automatically within ~1 minute, on every gateway at once.'
199+ }
147200 />
148201 </ View >
149202
@@ -165,10 +218,16 @@ function InAppPay({ payment }: { readonly payment: PaymentIdentity }): React.JSX
165218}
166219
167220/** Store-compliant "manage on the web" copy: no QR / address / purchase link. */
168- function ManageOnWeb ( { payment } : { readonly payment : PaymentIdentity } ) : React . JSX . Element {
221+ function ManageOnWeb ( {
222+ payment,
223+ premium,
224+ } : {
225+ readonly payment : PaymentIdentity ;
226+ readonly premium : boolean ;
227+ } ) : React . JSX . Element {
169228 return (
170229 < >
171- < Text style = { styles . section } > How to upgrade</ Text >
230+ < Text style = { styles . section } > { premium ? ' How to add more time' : 'How to upgrade' } </ Text >
172231 < View style = { styles . steps } >
173232 < Step n = { 1 } text = { `Open ${ UPGRADE_URL } in any browser — on this phone or a computer.` } />
174233 < Step
0 commit comments