1- import React , { useState } from "react" ;
1+ import React , { useState , useRef , useCallback } from "react" ;
22import {
33 View ,
44 Text ,
88 Modal ,
99 KeyboardAvoidingView ,
1010 Platform ,
11+ PanResponder ,
12+ GestureResponderEvent ,
13+ PanResponderGestureState ,
1114} from "react-native" ;
1215import { SafeAreaView } from "react-native-safe-area-context" ;
1316import { Ionicons } from "@expo/vector-icons" ;
@@ -33,6 +36,206 @@ const NAIRA_KEYPAD_KEYS = [
3336 "⌫" ,
3437] ;
3538
39+ // ── Percentage Slider ─────────────────────────────────────────────────────────
40+
41+ const SNAP_PERCENTAGES = [ 0 , 25 , 50 , 75 , 100 ] ;
42+
43+ interface PercentageSliderProps {
44+ /** Current selected percentage (0–100). */
45+ value : number ;
46+ /** Called with the snapped percentage when the user slides or taps a tick. */
47+ onChange : ( pct : number ) => void ;
48+ }
49+
50+ /**
51+ * Horizontal slider that snaps to 0 / 25 / 50 / 75 / 100 %.
52+ * Dragging the thumb or tapping a tick label both update the selection.
53+ */
54+ function PercentageSlider ( { value, onChange } : PercentageSliderProps ) {
55+ const trackWidth = useRef ( 0 ) ;
56+
57+ const snapToNearest = useCallback (
58+ ( rawPct : number ) => {
59+ const clamped = Math . max ( 0 , Math . min ( 100 , rawPct ) ) ;
60+ let closest = SNAP_PERCENTAGES [ 0 ] ;
61+ let minDist = Math . abs ( clamped - closest ) ;
62+ for ( const snap of SNAP_PERCENTAGES ) {
63+ const dist = Math . abs ( clamped - snap ) ;
64+ if ( dist < minDist ) {
65+ minDist = dist ;
66+ closest = snap ;
67+ }
68+ }
69+ onChange ( closest ) ;
70+ } ,
71+ [ onChange ]
72+ ) ;
73+
74+ const panResponder = useRef (
75+ PanResponder . create ( {
76+ onStartShouldSetPanResponder : ( ) => true ,
77+ onMoveShouldSetPanResponder : ( ) => true ,
78+ onPanResponderGrant : (
79+ evt : GestureResponderEvent ,
80+ _state : PanResponderGestureState
81+ ) => {
82+ if ( trackWidth . current === 0 ) return ;
83+ const x = evt . nativeEvent . locationX ;
84+ const rawPct = ( x / trackWidth . current ) * 100 ;
85+ snapToNearest ( rawPct ) ;
86+ } ,
87+ onPanResponderMove : (
88+ evt : GestureResponderEvent ,
89+ _state : PanResponderGestureState
90+ ) => {
91+ if ( trackWidth . current === 0 ) return ;
92+ const x = evt . nativeEvent . locationX ;
93+ const rawPct = ( x / trackWidth . current ) * 100 ;
94+ snapToNearest ( rawPct ) ;
95+ } ,
96+ } )
97+ ) . current ;
98+
99+ const thumbPosition = `${ value } %` as `${number } %`;
100+
101+ return (
102+ < View style = { sliderStyles . wrapper } >
103+ { /* Track */ }
104+ < View
105+ style = { sliderStyles . trackContainer }
106+ onLayout = { ( e ) => {
107+ trackWidth . current = e . nativeEvent . layout . width ;
108+ } }
109+ { ...panResponder . panHandlers }
110+ accessible = { false }
111+ >
112+ { /* Filled portion */ }
113+ < View style = { [ sliderStyles . trackFill , { width : thumbPosition } ] } />
114+ { /* Tick marks */ }
115+ { SNAP_PERCENTAGES . map ( ( pct ) => (
116+ < View
117+ key = { pct }
118+ style = { [
119+ sliderStyles . tick ,
120+ { left : `${ pct } %` as `${number } %` } ,
121+ pct === value && sliderStyles . tickActive ,
122+ ] }
123+ />
124+ ) ) }
125+ { /* Thumb */ }
126+ < View
127+ style = { [ sliderStyles . thumb , { left : thumbPosition } ] }
128+ accessibilityRole = "adjustable"
129+ accessibilityValue = { { min : 0 , max : 100 , now : value } }
130+ accessibilityLabel = { `Percentage slider, ${ value } %` }
131+ />
132+ </ View >
133+
134+ { /* Snap-point labels */ }
135+ < View style = { sliderStyles . labelsRow } >
136+ { SNAP_PERCENTAGES . filter ( ( p ) => p > 0 ) . map ( ( pct ) => (
137+ < TouchableOpacity
138+ key = { pct }
139+ style = { [
140+ sliderStyles . labelBtn ,
141+ pct === value && sliderStyles . labelBtnActive ,
142+ ] }
143+ onPress = { ( ) => onChange ( pct ) }
144+ activeOpacity = { 0.75 }
145+ accessibilityRole = "button"
146+ accessibilityLabel = { `Set ${ pct } percent` }
147+ >
148+ < Text
149+ style = { [
150+ sliderStyles . labelText ,
151+ pct === value && sliderStyles . labelTextActive ,
152+ ] }
153+ >
154+ { pct } %
155+ </ Text >
156+ </ TouchableOpacity >
157+ ) ) }
158+ </ View >
159+ </ View >
160+ ) ;
161+ }
162+
163+ const sliderStyles = StyleSheet . create ( {
164+ wrapper : {
165+ marginBottom : 20 ,
166+ } ,
167+ trackContainer : {
168+ height : 28 ,
169+ backgroundColor : "#E8E8E8" ,
170+ borderRadius : 14 ,
171+ marginHorizontal : 8 ,
172+ justifyContent : "center" ,
173+ overflow : "visible" ,
174+ position : "relative" ,
175+ } ,
176+ trackFill : {
177+ position : "absolute" ,
178+ left : 0 ,
179+ top : 0 ,
180+ bottom : 0 ,
181+ backgroundColor : COLORS . primary ,
182+ borderRadius : 14 ,
183+ } ,
184+ tick : {
185+ position : "absolute" ,
186+ width : 2 ,
187+ height : 10 ,
188+ backgroundColor : "#CCCCCC" ,
189+ borderRadius : 1 ,
190+ top : 9 ,
191+ marginLeft : - 1 ,
192+ } ,
193+ tickActive : {
194+ backgroundColor : COLORS . secondary ,
195+ } ,
196+ thumb : {
197+ position : "absolute" ,
198+ width : 28 ,
199+ height : 28 ,
200+ borderRadius : 14 ,
201+ backgroundColor : COLORS . white ,
202+ borderWidth : 2.5 ,
203+ borderColor : COLORS . primary ,
204+ shadowColor : "#000" ,
205+ shadowOffset : { width : 0 , height : 2 } ,
206+ shadowOpacity : 0.12 ,
207+ shadowRadius : 4 ,
208+ elevation : 3 ,
209+ marginLeft : - 14 ,
210+ top : 0 ,
211+ } ,
212+ labelsRow : {
213+ flexDirection : "row" ,
214+ justifyContent : "space-around" ,
215+ marginTop : 10 ,
216+ marginHorizontal : 8 ,
217+ } ,
218+ labelBtn : {
219+ paddingVertical : 6 ,
220+ paddingHorizontal : 12 ,
221+ borderRadius : 8 ,
222+ backgroundColor : "#F0F0F0" ,
223+ minWidth : 52 ,
224+ alignItems : "center" ,
225+ } ,
226+ labelBtnActive : {
227+ backgroundColor : COLORS . primary ,
228+ } ,
229+ labelText : {
230+ fontSize : 13 ,
231+ fontFamily : "Outfit_600SemiBold" ,
232+ color : COLORS . primary ,
233+ } ,
234+ labelTextActive : {
235+ color : COLORS . secondary ,
236+ } ,
237+ } ) ;
238+
36239// ── Naira Keypad ─────────────────────────────────────────────────────────────
37240
38241function NairaKeypad ( { onPress } : { onPress : ( key : string ) => void } ) {
@@ -60,6 +263,7 @@ export default function YieldTransactionScreen() {
60263 const [ amount , setAmount ] = useState ( "" ) ;
61264 const [ showConfirm , setShowConfirm ] = useState ( false ) ;
62265 const [ submitted , setSubmitted ] = useState ( false ) ;
266+ const [ sliderPct , setSliderPct ] = useState ( 0 ) ;
63267
64268 // Simulated balances — replace with API/store values in production
65269 const availableBalance = 150_000 ;
@@ -74,6 +278,8 @@ export default function YieldTransactionScreen() {
74278 const isValid = numericAmount > 0 && ! exceedsLimit ;
75279
76280 function handleKeyPress ( key : string ) {
281+ // Manual keypad entry clears the slider selection
282+ setSliderPct ( 0 ) ;
77283 if ( key === "⌫" ) {
78284 setAmount ( ( prev ) => prev . slice ( 0 , - 1 ) ) ;
79285 return ;
@@ -162,6 +368,7 @@ export default function YieldTransactionScreen() {
162368 onPress = { ( ) => {
163369 setMode ( "deposit" ) ;
164370 setAmount ( "" ) ;
371+ setSliderPct ( 0 ) ;
165372 } }
166373 >
167374 < Ionicons
@@ -187,6 +394,7 @@ export default function YieldTransactionScreen() {
187394 onPress = { ( ) => {
188395 setMode ( "withdraw" ) ;
189396 setAmount ( "" ) ;
397+ setSliderPct ( 0 ) ;
190398 } }
191399 >
192400 < Ionicons
@@ -242,20 +450,18 @@ export default function YieldTransactionScreen() {
242450 </ Text >
243451 ) }
244452
245- { /* Quick-fill buttons */ }
246- < View style = { styles . quickFillRow } >
247- { [ 25 , 50 , 75 , 100 ] . map ( ( pct ) => (
248- < TouchableOpacity
249- key = { pct }
250- style = { styles . quickFillBtn }
251- onPress = { ( ) =>
252- setAmount ( ( ( activeBalance * pct ) / 100 ) . toFixed ( 2 ) )
253- }
254- >
255- < Text style = { styles . quickFillText } > { pct } %</ Text >
256- </ TouchableOpacity >
257- ) ) }
258- </ View >
453+ { /* Percentage slider */ }
454+ < PercentageSlider
455+ value = { sliderPct }
456+ onChange = { ( pct ) => {
457+ setSliderPct ( pct ) ;
458+ if ( pct === 0 ) {
459+ setAmount ( "" ) ;
460+ } else {
461+ setAmount ( ( ( activeBalance * pct ) / 100 ) . toFixed ( 2 ) ) ;
462+ }
463+ } }
464+ />
259465
260466 { /* Naira keypad */ }
261467 < NairaKeypad onPress = { handleKeyPress } />
0 commit comments