enh
This commit is contained in:
parent
8e2e863b84
commit
1729b028d0
4 changed files with 110 additions and 41 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, ActivityIndicator } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { Slot } from 'expo-router';
|
import { Slot } from 'expo-router';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { MyText, MyTouchableOpacity, tw, AppContainer } from 'common-ui';
|
import { MyText, MyTouchableOpacity, tw, AppContainer } from 'common-ui';
|
||||||
|
|
@ -9,18 +9,7 @@ import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||||
|
|
||||||
export default function FlashDeliveryBaseLayout() {
|
export default function FlashDeliveryBaseLayout() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: essentialConsts, isLoading } = useGetEssentialConsts();
|
const { data: essentialConsts } = useGetEssentialConsts();
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<AppContainer>
|
|
||||||
<View style={tw`flex-1 justify-center items-center`}>
|
|
||||||
<ActivityIndicator size="large" color="#22c55e" />
|
|
||||||
<MyText style={tw`text-gray-500 mt-4`}>Loading...</MyText>
|
|
||||||
</View>
|
|
||||||
</AppContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFlashDeliveryEnabled = essentialConsts?.isFlashDeliveryEnabled ?? true;
|
const isFlashDeliveryEnabled = essentialConsts?.isFlashDeliveryEnabled ?? true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -514,7 +514,7 @@ export default function Dashboard() {
|
||||||
error,
|
error,
|
||||||
} = useAllProducts();
|
} = useAllProducts();
|
||||||
|
|
||||||
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
|
const { data: essentialConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
|
||||||
|
|
||||||
const { data: storesData, refetch: refetchStores } = useStores();
|
const { data: storesData, refetch: refetchStores } = useStores();
|
||||||
const { data: slotsData } = useSlots();
|
const { data: slotsData } = useSlots();
|
||||||
|
|
@ -685,11 +685,11 @@ export default function Dashboard() {
|
||||||
staticStyles.flatListContent
|
staticStyles.flatListContent
|
||||||
], []);
|
], []);
|
||||||
|
|
||||||
if (isLoading || isLoadingConsts) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
||||||
<MyText style={tw`text-gray-500 font-medium`}>
|
<MyText style={tw`text-gray-500 font-medium`}>
|
||||||
{isLoading ? 'Loading products...' : 'Loading app settings...'}
|
Loading products...
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, ActivityIndicator } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { WebView } from 'react-native-webview';
|
import { WebView } from 'react-native-webview';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
import { useGetEssentialConsts } from '@/src/hooks/prominent-api-hooks';
|
||||||
import { theme, MyText, MyTouchableOpacity } from 'common-ui';
|
import { MyTouchableOpacity } from 'common-ui';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
||||||
interface WebViewWrapperProps {
|
interface WebViewWrapperProps {
|
||||||
|
|
@ -11,18 +11,9 @@ interface WebViewWrapperProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WebViewWrapper({ children }: WebViewWrapperProps) {
|
export default function WebViewWrapper({ children }: WebViewWrapperProps) {
|
||||||
const { data: constsData, isLoading } = useGetEssentialConsts();
|
const { data: constsData } = useGetEssentialConsts();
|
||||||
const [isClosed, setIsClosed] = useState(false);
|
const [isClosed, setIsClosed] = useState(false);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: theme.colors.gray1 }}>
|
|
||||||
<ActivityIndicator size="large" color={theme.colors.brand500} />
|
|
||||||
<MyText style={{ color: theme.colors.gray500, marginTop: 16 }}>Loading...</MyText>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const webviewHtml = constsData?.webviewHtml;
|
const webviewHtml = constsData?.webviewHtml;
|
||||||
const isWebviewClosable = constsData?.isWebviewClosable;
|
const isWebviewClosable = constsData?.isWebviewClosable;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { trpc } from '@/src/trpc-client'
|
import { trpc } from '@/src/trpc-client'
|
||||||
import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType, AvailabilityApiType } from "@backend/trpc/router";
|
import { AllProductsApiType, StoresApiType, SlotsApiType, EssentialConstsApiType, BannersApiType, StoreWithProductsApiType, AvailabilityApiType } from "@backend/trpc/router";
|
||||||
import { CACHE_FILENAMES } from "@packages/shared";
|
import { CACHE_FILENAMES } from "@packages/shared";
|
||||||
|
import { StorageServiceCasual } from 'common-ui';
|
||||||
|
|
||||||
// Local useGetEssentialConsts hook
|
// Local useGetEssentialConsts hook
|
||||||
export const useGetEssentialConsts = () => {
|
export const useGetEssentialConsts = () => {
|
||||||
|
|
@ -13,6 +14,70 @@ export const useGetEssentialConsts = () => {
|
||||||
return { ...query, refetch: query.refetch }
|
return { ...query, refetch: query.refetch }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Persisted cache helpers — store fetched JSON keyed by its versioned URL.
|
||||||
|
// Only refetch + repersist when the version (URL) changes.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface PersistedCache<T> {
|
||||||
|
version: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_STORAGE_KEYS = {
|
||||||
|
products: 'cache:products',
|
||||||
|
stores: 'cache:stores',
|
||||||
|
slots: 'cache:slots',
|
||||||
|
banners: 'cache:banners',
|
||||||
|
availability: 'cache:availability',
|
||||||
|
storeProducts: (storeId: number) => `cache:store:${storeId}`,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
async function readPersistedCache<T>(key: string): Promise<PersistedCache<T> | null> {
|
||||||
|
const raw = await StorageServiceCasual.getItem(key)
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as PersistedCache<T>
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePersistedCache<T>(key: string, version: string, data: T): Promise<void> {
|
||||||
|
await StorageServiceCasual.setItem(key, JSON.stringify({ version, data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the persisted cache for `storageKey` and reports whether its version
|
||||||
|
* matches the current `version`. Returns { initialData, isReady }.
|
||||||
|
* - version matches → serve persisted data, no network refetch needed.
|
||||||
|
* - version differs → signal to refetch (caller's query fetches + repersists).
|
||||||
|
*/
|
||||||
|
function usePersistedCache<T>(storageKey: string, version: string | null) {
|
||||||
|
const [initialData, setInitialData] = useState<T | undefined>(undefined)
|
||||||
|
const [isReady, setIsReady] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setIsReady(false)
|
||||||
|
setInitialData(undefined)
|
||||||
|
if (!version) {
|
||||||
|
setIsReady(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readPersistedCache<T>(storageKey).then((persisted) => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (persisted && persisted.version === version) {
|
||||||
|
setInitialData(persisted.data)
|
||||||
|
}
|
||||||
|
setIsReady(true)
|
||||||
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [storageKey, version])
|
||||||
|
|
||||||
|
return { initialData, isReady }
|
||||||
|
}
|
||||||
|
|
||||||
type ProductsResponse = AllProductsApiType;
|
type ProductsResponse = AllProductsApiType;
|
||||||
type StoresResponse = StoresApiType;
|
type StoresResponse = StoresApiType;
|
||||||
type SlotsResponse = SlotsApiType;
|
type SlotsResponse = SlotsApiType;
|
||||||
|
|
@ -76,18 +141,22 @@ function useSlotsCacheUrl(): string | null {
|
||||||
export function useAllProducts() {
|
export function useAllProducts() {
|
||||||
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
|
const cacheUrl = useCacheUrl(CACHE_FILENAMES.products)
|
||||||
const { data: availabilityData } = useAvailability()
|
const { data: availabilityData } = useAvailability()
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<ProductsResponse>(CACHE_STORAGE_KEYS.products, version)
|
||||||
|
|
||||||
const productsQuery = useQuery<ProductsResponse>({
|
const productsQuery = useQuery<ProductsResponse>({
|
||||||
queryKey: ['all-products', cacheUrl],
|
queryKey: ['all-products', version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<ProductsResponse>(cacheUrl)
|
const response = await axios.get<ProductsResponse>(cacheUrl)
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.products, version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
|
|
||||||
const mergedProducts = React.useMemo(() => {
|
const mergedProducts = React.useMemo(() => {
|
||||||
|
|
@ -127,71 +196,87 @@ export function useAllProducts() {
|
||||||
|
|
||||||
export function useAvailability() {
|
export function useAvailability() {
|
||||||
const cacheUrl = useAvailabilityCacheUrl()
|
const cacheUrl = useAvailabilityCacheUrl()
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<AvailabilityResponse>(CACHE_STORAGE_KEYS.availability, version)
|
||||||
|
|
||||||
return useQuery<AvailabilityResponse>({
|
return useQuery<AvailabilityResponse>({
|
||||||
queryKey: ['availability', cacheUrl],
|
queryKey: ['availability', version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<AvailabilityResponse>(cacheUrl)
|
const response = await axios.get<AvailabilityResponse>(cacheUrl)
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.availability, version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useStores() {
|
export function useStores() {
|
||||||
const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores)
|
const cacheUrl = useCacheUrl(CACHE_FILENAMES.stores)
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<StoresResponse>(CACHE_STORAGE_KEYS.stores, version)
|
||||||
|
|
||||||
return useQuery<StoresResponse>({
|
return useQuery<StoresResponse>({
|
||||||
queryKey: ['stores', cacheUrl],
|
queryKey: ['stores', version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<StoresResponse>(cacheUrl)
|
const response = await axios.get<StoresResponse>(cacheUrl)
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.stores, version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSlots() {
|
export function useSlots() {
|
||||||
const cacheUrl = useSlotsCacheUrl()
|
const cacheUrl = useSlotsCacheUrl()
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<SlotsResponse>(CACHE_STORAGE_KEYS.slots, version)
|
||||||
|
|
||||||
return useQuery<SlotsResponse>({
|
return useQuery<SlotsResponse>({
|
||||||
queryKey: ['slots', cacheUrl],
|
queryKey: ['slots', version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<SlotsResponse>(cacheUrl+'?v=123')
|
const response = await axios.get<SlotsResponse>(cacheUrl+'?v=123')
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.slots, version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBanners() {
|
export function useBanners() {
|
||||||
const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners)
|
const cacheUrl = useCacheUrl(CACHE_FILENAMES.banners)
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<BannersResponse>(CACHE_STORAGE_KEYS.banners, version)
|
||||||
|
|
||||||
return useQuery<BannersResponse>({
|
return useQuery<BannersResponse>({
|
||||||
queryKey: ['banners', cacheUrl],
|
queryKey: ['banners', version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<BannersResponse>(cacheUrl)
|
const response = await axios.get<BannersResponse>(cacheUrl)
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.banners, version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
|
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,17 +290,21 @@ export function useStoreWithProducts(storeId: number) {
|
||||||
const cacheUrl = assetsDomain && apiCacheKey
|
const cacheUrl = assetsDomain && apiCacheKey
|
||||||
? `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/stores/${storeId}.json`
|
? `${assetsDomain}${apiCacheKey}/v-${cacheVersion}/stores/${storeId}.json`
|
||||||
: null
|
: null
|
||||||
|
const version = cacheUrl ?? ''
|
||||||
|
const { initialData, isReady } = usePersistedCache<StoreWithProductsResponse>(CACHE_STORAGE_KEYS.storeProducts(storeId), version)
|
||||||
|
|
||||||
return useQuery<StoreWithProductsResponse>({
|
return useQuery<StoreWithProductsResponse>({
|
||||||
queryKey: ['store-with-products', storeId, cacheUrl],
|
queryKey: ['store-with-products', storeId, version],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!cacheUrl) {
|
if (!cacheUrl) {
|
||||||
throw new Error('Cache URL not available')
|
throw new Error('Cache URL not available')
|
||||||
}
|
}
|
||||||
const response = await axios.get<StoreWithProductsResponse>(cacheUrl)
|
const response = await axios.get<StoreWithProductsResponse>(cacheUrl)
|
||||||
|
await writePersistedCache(CACHE_STORAGE_KEYS.storeProducts(storeId), version, response.data)
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
staleTime: 60000, // 1 minute
|
staleTime: 60000, // 1 minute
|
||||||
enabled: !!cacheUrl,
|
enabled: !!cacheUrl && isReady,
|
||||||
|
placeholderData: initialData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue