@@ -124,6 +124,82 @@ let socialBindingPreview = $state<SocialBindingDisplay[]>(
124124);
125125const verified = $derived (isFake === false || legalId !== null );
126126
127+ // ── Pull-to-refresh ───────────────────────────────────────────────────────────
128+ const PULL_THRESHOLD = 72 ;
129+ const PULL_MAX = 100 ;
130+ let pullState = $state <" idle" | " pulling" | " triggered" | " refreshing" >(" idle" );
131+ let pullDistance = $state (0 );
132+ let _pullStartY = 0 ;
133+ let _pullTracking = false ;
134+
135+ function getPullScrollTop(): number {
136+ const el = document .querySelector (" [data-route-wrapper]" ) as HTMLElement | null ;
137+ return el ?.scrollTop ?? 0 ;
138+ }
139+
140+ function onPullTouchStart(e : TouchEvent ) {
141+ if (tourStep !== null ) return ;
142+ if (pullState === " refreshing" ) return ;
143+ if (getPullScrollTop () > 4 ) return ;
144+ _pullStartY = e .touches [0 ].clientY ;
145+ _pullTracking = true ;
146+ }
147+
148+ function onPullTouchMove(e : TouchEvent ) {
149+ if (! _pullTracking || pullState === " refreshing" ) return ;
150+ const delta = e .touches [0 ].clientY - _pullStartY ;
151+ if (delta <= 0 ) {
152+ _pullTracking = false ;
153+ pullState = " idle" ;
154+ pullDistance = 0 ;
155+ return ;
156+ }
157+ e .preventDefault ();
158+ pullDistance = Math .min (delta * 0.55 , PULL_MAX );
159+ pullState = pullDistance >= PULL_THRESHOLD ? " triggered" : " pulling" ;
160+ }
161+
162+ function onPullTouchEnd() {
163+ if (! _pullTracking ) return ;
164+ _pullTracking = false ;
165+ if (pullState === " triggered" ) {
166+ pullState = " refreshing" ;
167+ pullDistance = PULL_THRESHOLD ;
168+ void refreshBindings ().finally (() => {
169+ pullState = " idle" ;
170+ pullDistance = 0 ;
171+ });
172+ } else {
173+ pullState = " idle" ;
174+ pullDistance = 0 ;
175+ }
176+ }
177+
178+ function onPullTouchCancel() {
179+ _pullTracking = false ;
180+ if (pullState !== " refreshing" ) {
181+ pullState = " idle" ;
182+ pullDistance = 0 ;
183+ }
184+ }
185+
186+ async function loadUserInfo(): Promise <void > {
187+ if (! globalState ) return ;
188+ const [userInfo, fake, vaultData] = await Promise .all ([
189+ globalState .userController .user ,
190+ globalState .userController .isFake ,
191+ globalState .vaultController .vault ,
192+ ]);
193+ isFake = fake ;
194+ cachedIsFake = fake ;
195+ userData = { ... userInfo , isFake: fake };
196+ cachedUserData = userData ;
197+ if (vaultData ?.ename ) {
198+ ename = vaultData .ename ;
199+ cachedEname = ename ;
200+ }
201+ }
202+
127203function openKycFlow() {
128204 kycOpen = true ;
129205}
@@ -753,15 +829,7 @@ onMount(() => {
753829
754830 profileCreationStatus = gs .vaultController .profileCreationStatus ;
755831
756- const userInfo = await gs .userController .user ;
757- const fake = await gs .userController .isFake ;
758- isFake = fake ;
759- cachedIsFake = fake ;
760- userData = { ... userInfo , isFake: fake };
761- cachedUserData = userData ;
762- const vaultData = await gs .vaultController .vault ;
763- ename = vaultData ?.ename ;
764- cachedEname = ename ;
832+ await loadUserInfo ();
765833
766834 void Promise .allSettled ([
767835 loadBindingDocuments (),
@@ -807,6 +875,10 @@ onMount(() => {
807875 };
808876 if (typeof document !== " undefined" ) {
809877 document .addEventListener (" visibilitychange" , onVisibility );
878+ document .addEventListener (" touchstart" , onPullTouchStart , { passive: true });
879+ document .addEventListener (" touchmove" , onPullTouchMove , { passive: false });
880+ document .addEventListener (" touchend" , onPullTouchEnd , { passive: true });
881+ document .addEventListener (" touchcancel" , onPullTouchCancel , { passive: true });
810882 }
811883});
812884
@@ -817,8 +889,12 @@ onDestroy(() => {
817889 if (bindingsRefreshInterval ) {
818890 clearInterval (bindingsRefreshInterval );
819891 }
820- if (typeof document !== " undefined" && onVisibility ) {
821- document .removeEventListener (" visibilitychange" , onVisibility );
892+ if (typeof document !== " undefined" ) {
893+ if (onVisibility ) document .removeEventListener (" visibilitychange" , onVisibility );
894+ document .removeEventListener (" touchstart" , onPullTouchStart );
895+ document .removeEventListener (" touchmove" , onPullTouchMove );
896+ document .removeEventListener (" touchend" , onPullTouchEnd );
897+ document .removeEventListener (" touchcancel" , onPullTouchCancel );
822898 }
823899 unsubNotifications ?.();
824900});
@@ -831,6 +907,7 @@ async function refreshBindings(): Promise<void> {
831907 if (! globalState ) return ;
832908 try {
833909 await Promise .all ([
910+ loadUserInfo (),
834911 loadBindingDocuments (),
835912 loadPersonalIntoStore (),
836913 ]);
@@ -871,6 +948,37 @@ async function refreshBindings(): Promise<void> {
871948 </div >
872949 </div >
873950{:else }
951+ <!-- Pull-to-refresh indicator: slides in from top as the user drags down from scroll top -->
952+ {#if pullState !== " idle" }
953+ <div
954+ aria-hidden =" true"
955+ class =" fixed left-0 right-0 flex justify-center pointer-events-none z-50"
956+ style ="top: calc(env(safe-area-inset-top) + {pullDistance - 48 }px); transition: {pullState === ' refreshing' ? ' top 200ms ease-out' : ' none' };"
957+ >
958+ <div class =" bg-white rounded-full p-2.5 shadow-md border border-gray-100" >
959+ {#if pullState === " refreshing" }
960+ <Shadow size ={22 } color =" rgb(142, 82, 255)" />
961+ {:else }
962+ <svg
963+ xmlns =" http://www.w3.org/2000/svg"
964+ width =" 22"
965+ height =" 22"
966+ viewBox =" 0 0 24 24"
967+ fill =" none"
968+ stroke =" rgb(142, 82, 255)"
969+ stroke-width =" 2.5"
970+ stroke-linecap =" round"
971+ stroke-linejoin =" round"
972+ style ="transform: rotate( {pullState === ' triggered' ? 180 : 0 }deg); transition: transform 200ms ease;"
973+ >
974+ <path d =" M12 5v14" />
975+ <path d =" M5 12l7 7 7-7" />
976+ </svg >
977+ {/if }
978+ </div >
979+ </div >
980+ {/if }
981+
874982 <div
875983 class =" relative transition-transform duration-500 ease-out will-change-transform"
876984 style ="padding-bottom: max(16px, env(safe-area-inset-bottom)); transform: translateY(- {tourOffset }px);"
0 commit comments