Skip to content

Commit d6b6906

Browse files
committed
feat: added pull-to-refresh on home screen.
1 parent 5f08b54 commit d6b6906

1 file changed

Lines changed: 119 additions & 11 deletions

File tree

infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,82 @@ let socialBindingPreview = $state<SocialBindingDisplay[]>(
124124
);
125125
const 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+
127203
function 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

Comments
 (0)