@wasto/cms-components 1.5.10 → 1.5.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -47
- package/dist/index.d.mts +181 -51
- package/dist/index.d.ts +181 -51
- package/dist/index.js +3614 -3015
- package/dist/index.mjs +3021 -2418
- package/package.json +12 -2
- package/src/components/announcement-modal/AnnouncementModal.tsx +183 -183
- package/src/components/article/Article.tsx +21 -21
- package/src/components/article/ArticleCard.tsx +81 -81
- package/src/components/article/ArticleExcerpts.tsx +80 -80
- package/src/components/article/ArticleList.tsx +114 -114
- package/src/components/article/ArticleMarkdown.tsx +170 -170
- package/src/components/article/ArticleReaderModal.tsx +93 -93
- package/src/components/article/ArticleSingle.tsx +57 -57
- package/src/components/bottom-tab/BottomTab.tsx +392 -392
- package/src/components/button/Button.tsx +154 -154
- package/src/components/challenge/Badges.tsx +84 -84
- package/src/components/challenge/BadgesModal.tsx +166 -166
- package/src/components/challenge/Challenge.tsx +380 -380
- package/src/components/challenge/ChallengeModal.tsx +231 -231
- package/src/components/challenge/ChallengeVignette.tsx +249 -249
- package/src/components/challenge/ChallengesModal.tsx +243 -243
- package/src/components/challenge/QuizModal.tsx +381 -381
- package/src/components/challenge/ResultModal.tsx +144 -144
- package/src/components/chat/Chat.tsx +928 -928
- package/src/components/collection-calendar/CollectionCalendar.tsx +274 -274
- package/src/components/columns/Columns.tsx +33 -33
- package/src/components/consignes-modal/ConsignesModal.tsx +267 -267
- package/src/components/content-inset/ContentInset.tsx +33 -33
- package/src/components/header/Header.tsx +415 -391
- package/src/components/image/Image.tsx +15 -15
- package/src/components/image/ImageFrame.tsx +102 -102
- package/src/components/image/ImageMedia.tsx +31 -31
- package/src/components/image/ImageUrl.tsx +30 -30
- package/src/components/locations-map/LocationSheet.tsx +417 -417
- package/src/components/locations-map/LocationsMap.native.tsx +260 -260
- package/src/components/locations-map/LocationsMap.tsx +13 -13
- package/src/components/locations-map/LocationsMap.web.tsx +10 -10
- package/src/components/locations-map/LocationsMapShell.tsx +546 -527
- package/src/components/locations-map/LocationsMapView.tsx +108 -108
- package/src/components/locations-map/LocationsMapView.web.tsx +331 -331
- package/src/components/nav-button/NavButton.tsx +59 -59
- package/src/components/next-collection/NextCollection.tsx +636 -621
- package/src/components/notifications/NotificationsScreen.tsx +247 -247
- package/src/components/page/Page.tsx +102 -64
- package/src/components/parapraph/Paragraph.tsx +67 -67
- package/src/components/profile/ProfileScreen.tsx +236 -236
- package/src/components/push/PushTokenRegistrar.tsx +47 -0
- package/src/components/recycling-info/RecyclingInfo.tsx +189 -189
- package/src/components/safe-area/OptionalSafeAreaProvider.tsx +17 -17
- package/src/components/search/Search.tsx +320 -320
- package/src/components/select-street-modal/SelectStreetModal.tsx +475 -475
- package/src/components/title/Title.tsx +100 -100
- package/src/components/vision/Vision.tsx +447 -447
- package/src/components/vision/VisionActionCard.tsx +117 -117
- package/src/components/vision/VisionCapture.tsx +30 -30
- package/src/components/vision/VisionTapOverlay.tsx +122 -122
- package/src/contexts/AnnouncementsContext.tsx +171 -171
- package/src/contexts/AppDataContext.tsx +458 -0
- package/src/contexts/ChallengesContext.tsx +182 -182
- package/src/contexts/HeaderConfigContext.tsx +29 -0
- package/src/contexts/LocalContext.tsx +227 -227
- package/src/contexts/ThemeContext.tsx +37 -37
- package/src/contexts/WastoContext.tsx +132 -127
- package/src/helpers/blogPost.ts +72 -72
- package/src/helpers/generateQuizQuestions.ts +247 -247
- package/src/helpers/getContrastColor.ts +18 -18
- package/src/helpers/haptics.ts +20 -20
- package/src/helpers/hoursSchedule.ts +230 -230
- package/src/helpers/htmlContentToMarkdown.ts +114 -114
- package/src/helpers/layout.ts +54 -54
- package/src/helpers/loadLinearGradient.ts +22 -22
- package/src/helpers/mapLocations.ts +170 -0
- package/src/helpers/maquetteTheme.ts +76 -76
- package/src/helpers/markdownUnderlinePlugin.ts +32 -32
- package/src/helpers/nextCollection.ts +122 -0
- package/src/helpers/normalizeWasteTypeRecyclingInfo.ts +80 -80
- package/src/helpers/publicMediaUrl.ts +8 -8
- package/src/helpers/resolveApiAssetUrl.ts +21 -21
- package/src/helpers/systemRoutes.ts +7 -7
- package/src/helpers/themeStyles.ts +114 -114
- package/src/helpers/ttlCache.ts +41 -0
- package/src/helpers/useSafeAreaPadding.ts +14 -14
- package/src/hooks/useBadges.ts +100 -100
- package/src/hooks/useBlogPosts.ts +109 -105
- package/src/hooks/useChallenges.ts +110 -110
- package/src/hooks/useCities.ts +56 -56
- package/src/hooks/useCityAnnouncements.ts +118 -118
- package/src/hooks/useCollectionCalendar.ts +421 -421
- package/src/hooks/useColumns.ts +19 -19
- package/src/hooks/useConsignes.ts +62 -62
- package/src/hooks/useLocations.ts +120 -248
- package/src/hooks/useNextCollection.ts +163 -238
- package/src/hooks/useRecyclingInfo.ts +90 -90
- package/src/hooks/useWastoClient.ts +16 -16
- package/src/index.ts +161 -142
- package/src/navigation/NavigationContext.tsx +56 -56
- package/src/push/registerExpoPushToken.ts +133 -0
- package/src/sentry/initSentry.ts +120 -120
- package/src/types/Theme.ts +72 -72
- package/src/types/css-modules.d.ts +1 -1
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
-
import {
|
|
3
|
-
createContext,
|
|
4
|
-
PropsWithChildren,
|
|
5
|
-
useCallback,
|
|
6
|
-
useContext,
|
|
7
|
-
useEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useState,
|
|
10
|
-
type ReactNode,
|
|
11
|
-
} from "react";
|
|
12
|
-
import { Platform } from "react-native";
|
|
13
|
-
import { useWastoClient } from "../hooks/useWastoClient";
|
|
14
|
-
|
|
15
|
-
const STORAGE_CITY_KEY = "selected_city_id";
|
|
16
|
-
const STORAGE_PLACE_KEY = "selected_place_id";
|
|
17
|
-
const STORAGE_PLACE_META_KEY = "selected_place_meta";
|
|
18
|
-
const STORAGE_DEVICE_KEY = "wasto_device_id";
|
|
19
|
-
const STORAGE_DISMISSED_ANNOUNCEMENTS_KEY = "dismissed_announcements";
|
|
20
|
-
|
|
21
|
-
/** Isolate Studio preview storage from the real PWA device profile. */
|
|
22
|
-
const PREVIEW_PREFIX = "wasto_preview_";
|
|
23
|
-
|
|
24
|
-
export type PlaceSelectionMeta = {
|
|
25
|
-
cityName?: string | null;
|
|
26
|
-
streetName?: string | null;
|
|
27
|
-
sectorName?: string | null;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type LocalContextType = {
|
|
31
|
-
isLoading: boolean;
|
|
32
|
-
/** True when both city and collection place (sector) are selected. */
|
|
33
|
-
hasPlaceSelected: boolean;
|
|
34
|
-
/** @deprecated Prefer `hasPlaceSelected`. Kept for transitional callers. */
|
|
35
|
-
hasCitySelected: boolean;
|
|
36
|
-
selectedCityId: string | null;
|
|
37
|
-
selectedPlaceId: string | null;
|
|
38
|
-
placeMeta: PlaceSelectionMeta | null;
|
|
39
|
-
deviceId: string | null;
|
|
40
|
-
registerDevice: (
|
|
41
|
-
cityId: string,
|
|
42
|
-
placeId: string,
|
|
43
|
-
meta?: PlaceSelectionMeta,
|
|
44
|
-
) => Promise<string | null>;
|
|
45
|
-
/** Clears city/place selection (keeps deviceId) — reopen sector picker. */
|
|
46
|
-
clearPlaceSelection: () => Promise<void>;
|
|
47
|
-
/** Wipes local profile data including deviceId and dismissed announcements. */
|
|
48
|
-
clearLocalData: () => Promise<void>;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const LocalContext = createContext<LocalContextType | undefined>(undefined);
|
|
52
|
-
|
|
53
|
-
export type LocalContextProps = PropsWithChildren & {
|
|
54
|
-
previewMode?: boolean;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function storageKey(base: string, previewMode: boolean) {
|
|
58
|
-
return previewMode ? `${PREVIEW_PREFIX}${base}` : base;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function LocalProvider({
|
|
62
|
-
children,
|
|
63
|
-
previewMode = false,
|
|
64
|
-
}: LocalContextProps): ReactNode {
|
|
65
|
-
const keys = useMemo(
|
|
66
|
-
() => ({
|
|
67
|
-
city: storageKey(STORAGE_CITY_KEY, previewMode),
|
|
68
|
-
place: storageKey(STORAGE_PLACE_KEY, previewMode),
|
|
69
|
-
placeMeta: storageKey(STORAGE_PLACE_META_KEY, previewMode),
|
|
70
|
-
device: storageKey(STORAGE_DEVICE_KEY, previewMode),
|
|
71
|
-
dismissed: storageKey(STORAGE_DISMISSED_ANNOUNCEMENTS_KEY, previewMode),
|
|
72
|
-
}),
|
|
73
|
-
[previewMode],
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
77
|
-
const [hasPlaceSelected, setHasPlaceSelected] = useState(false);
|
|
78
|
-
const [selectedCityId, setSelectedCityId] = useState<string | null>(null);
|
|
79
|
-
const [selectedPlaceId, setSelectedPlaceId] = useState<string | null>(null);
|
|
80
|
-
const [placeMeta, setPlaceMeta] = useState<PlaceSelectionMeta | null>(null);
|
|
81
|
-
const [deviceId, setDeviceId] = useState<string | null>(null);
|
|
82
|
-
|
|
83
|
-
const client = useWastoClient();
|
|
84
|
-
|
|
85
|
-
const registerDevice = useCallback(
|
|
86
|
-
async (cityId: string, placeId: string, meta?: PlaceSelectionMeta) => {
|
|
87
|
-
setIsLoading(true);
|
|
88
|
-
const nextMeta: PlaceSelectionMeta = {
|
|
89
|
-
cityName: meta?.cityName?.trim() || null,
|
|
90
|
-
streetName: meta?.streetName?.trim() || null,
|
|
91
|
-
sectorName: meta?.sectorName?.trim() || null,
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
let resolvedDeviceId: string | null = null;
|
|
95
|
-
if (!previewMode) {
|
|
96
|
-
const newDeviceId = generateDeviceId();
|
|
97
|
-
await client.public.analytics.track({
|
|
98
|
-
id: newDeviceId,
|
|
99
|
-
cityId: cityId,
|
|
100
|
-
platform: Platform.OS,
|
|
101
|
-
osVersion: null,
|
|
102
|
-
appVersion: null,
|
|
103
|
-
deviceModel: null,
|
|
104
|
-
});
|
|
105
|
-
await AsyncStorage.setItem(keys.device, newDeviceId);
|
|
106
|
-
setDeviceId(newDeviceId);
|
|
107
|
-
resolvedDeviceId = newDeviceId;
|
|
108
|
-
} else {
|
|
109
|
-
// Studio preview: never create analytics devices / persist device id.
|
|
110
|
-
setDeviceId(null);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await AsyncStorage.setItem(keys.city, cityId);
|
|
114
|
-
await AsyncStorage.setItem(keys.place, placeId);
|
|
115
|
-
await AsyncStorage.setItem(keys.placeMeta, JSON.stringify(nextMeta));
|
|
116
|
-
|
|
117
|
-
setHasPlaceSelected(true);
|
|
118
|
-
setSelectedCityId(cityId);
|
|
119
|
-
setSelectedPlaceId(placeId);
|
|
120
|
-
setPlaceMeta(nextMeta);
|
|
121
|
-
setIsLoading(false);
|
|
122
|
-
return resolvedDeviceId;
|
|
123
|
-
},
|
|
124
|
-
[client.public.analytics, keys, previewMode],
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const clearPlaceSelection = useCallback(async () => {
|
|
128
|
-
await AsyncStorage.multiRemove([keys.city, keys.place, keys.placeMeta]);
|
|
129
|
-
setHasPlaceSelected(false);
|
|
130
|
-
setSelectedCityId(null);
|
|
131
|
-
setSelectedPlaceId(null);
|
|
132
|
-
setPlaceMeta(null);
|
|
133
|
-
}, [keys]);
|
|
134
|
-
|
|
135
|
-
const clearLocalData = useCallback(async () => {
|
|
136
|
-
await AsyncStorage.multiRemove([
|
|
137
|
-
keys.city,
|
|
138
|
-
keys.place,
|
|
139
|
-
keys.placeMeta,
|
|
140
|
-
keys.device,
|
|
141
|
-
keys.dismissed,
|
|
142
|
-
]);
|
|
143
|
-
setHasPlaceSelected(false);
|
|
144
|
-
setSelectedCityId(null);
|
|
145
|
-
setSelectedPlaceId(null);
|
|
146
|
-
setPlaceMeta(null);
|
|
147
|
-
setDeviceId(null);
|
|
148
|
-
}, [keys]);
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
const loadLocalData = async () => {
|
|
152
|
-
setIsLoading(true);
|
|
153
|
-
|
|
154
|
-
const [
|
|
155
|
-
localSelectedCityId,
|
|
156
|
-
localSelectedPlaceId,
|
|
157
|
-
localPlaceMetaRaw,
|
|
158
|
-
localDeviceId,
|
|
159
|
-
] = await Promise.all([
|
|
160
|
-
AsyncStorage.getItem(keys.city),
|
|
161
|
-
AsyncStorage.getItem(keys.place),
|
|
162
|
-
AsyncStorage.getItem(keys.placeMeta),
|
|
163
|
-
previewMode ? Promise.resolve(null) : AsyncStorage.getItem(keys.device),
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
let localPlaceMeta: PlaceSelectionMeta | null = null;
|
|
167
|
-
if (localPlaceMetaRaw) {
|
|
168
|
-
try {
|
|
169
|
-
const parsed = JSON.parse(localPlaceMetaRaw) as PlaceSelectionMeta;
|
|
170
|
-
localPlaceMeta = {
|
|
171
|
-
cityName: parsed.cityName ?? null,
|
|
172
|
-
streetName: parsed.streetName ?? null,
|
|
173
|
-
sectorName: parsed.sectorName ?? null,
|
|
174
|
-
};
|
|
175
|
-
} catch {
|
|
176
|
-
localPlaceMeta = null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const ready = Boolean(localSelectedCityId && localSelectedPlaceId);
|
|
181
|
-
setHasPlaceSelected(ready);
|
|
182
|
-
setSelectedCityId(localSelectedCityId);
|
|
183
|
-
setSelectedPlaceId(localSelectedPlaceId);
|
|
184
|
-
setPlaceMeta(localPlaceMeta);
|
|
185
|
-
setDeviceId(previewMode ? null : localDeviceId);
|
|
186
|
-
|
|
187
|
-
setIsLoading(false);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
void loadLocalData();
|
|
191
|
-
}, [keys, previewMode]);
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<LocalContext
|
|
195
|
-
value={{
|
|
196
|
-
isLoading,
|
|
197
|
-
hasPlaceSelected,
|
|
198
|
-
hasCitySelected: hasPlaceSelected,
|
|
199
|
-
selectedCityId,
|
|
200
|
-
selectedPlaceId,
|
|
201
|
-
placeMeta,
|
|
202
|
-
deviceId,
|
|
203
|
-
registerDevice,
|
|
204
|
-
clearPlaceSelection,
|
|
205
|
-
clearLocalData,
|
|
206
|
-
}}
|
|
207
|
-
>
|
|
208
|
-
{children}
|
|
209
|
-
</LocalContext>
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function useLocal() {
|
|
214
|
-
const context = useContext(LocalContext);
|
|
215
|
-
if (!context) {
|
|
216
|
-
throw new Error("useLocal must be within a LocalProvider.");
|
|
217
|
-
}
|
|
218
|
-
return context;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function generateDeviceId() {
|
|
222
|
-
const randomCharacters = Array.from({ length: 8 }, () =>
|
|
223
|
-
Math.floor(Math.random() * 10),
|
|
224
|
-
).join("");
|
|
225
|
-
|
|
226
|
-
return `device--${Date.now()}--${Platform.OS}--${randomCharacters}`;
|
|
227
|
-
}
|
|
1
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
PropsWithChildren,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { Platform } from "react-native";
|
|
13
|
+
import { useWastoClient } from "../hooks/useWastoClient";
|
|
14
|
+
|
|
15
|
+
const STORAGE_CITY_KEY = "selected_city_id";
|
|
16
|
+
const STORAGE_PLACE_KEY = "selected_place_id";
|
|
17
|
+
const STORAGE_PLACE_META_KEY = "selected_place_meta";
|
|
18
|
+
const STORAGE_DEVICE_KEY = "wasto_device_id";
|
|
19
|
+
const STORAGE_DISMISSED_ANNOUNCEMENTS_KEY = "dismissed_announcements";
|
|
20
|
+
|
|
21
|
+
/** Isolate Studio preview storage from the real PWA device profile. */
|
|
22
|
+
const PREVIEW_PREFIX = "wasto_preview_";
|
|
23
|
+
|
|
24
|
+
export type PlaceSelectionMeta = {
|
|
25
|
+
cityName?: string | null;
|
|
26
|
+
streetName?: string | null;
|
|
27
|
+
sectorName?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type LocalContextType = {
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
/** True when both city and collection place (sector) are selected. */
|
|
33
|
+
hasPlaceSelected: boolean;
|
|
34
|
+
/** @deprecated Prefer `hasPlaceSelected`. Kept for transitional callers. */
|
|
35
|
+
hasCitySelected: boolean;
|
|
36
|
+
selectedCityId: string | null;
|
|
37
|
+
selectedPlaceId: string | null;
|
|
38
|
+
placeMeta: PlaceSelectionMeta | null;
|
|
39
|
+
deviceId: string | null;
|
|
40
|
+
registerDevice: (
|
|
41
|
+
cityId: string,
|
|
42
|
+
placeId: string,
|
|
43
|
+
meta?: PlaceSelectionMeta,
|
|
44
|
+
) => Promise<string | null>;
|
|
45
|
+
/** Clears city/place selection (keeps deviceId) — reopen sector picker. */
|
|
46
|
+
clearPlaceSelection: () => Promise<void>;
|
|
47
|
+
/** Wipes local profile data including deviceId and dismissed announcements. */
|
|
48
|
+
clearLocalData: () => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const LocalContext = createContext<LocalContextType | undefined>(undefined);
|
|
52
|
+
|
|
53
|
+
export type LocalContextProps = PropsWithChildren & {
|
|
54
|
+
previewMode?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function storageKey(base: string, previewMode: boolean) {
|
|
58
|
+
return previewMode ? `${PREVIEW_PREFIX}${base}` : base;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function LocalProvider({
|
|
62
|
+
children,
|
|
63
|
+
previewMode = false,
|
|
64
|
+
}: LocalContextProps): ReactNode {
|
|
65
|
+
const keys = useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
city: storageKey(STORAGE_CITY_KEY, previewMode),
|
|
68
|
+
place: storageKey(STORAGE_PLACE_KEY, previewMode),
|
|
69
|
+
placeMeta: storageKey(STORAGE_PLACE_META_KEY, previewMode),
|
|
70
|
+
device: storageKey(STORAGE_DEVICE_KEY, previewMode),
|
|
71
|
+
dismissed: storageKey(STORAGE_DISMISSED_ANNOUNCEMENTS_KEY, previewMode),
|
|
72
|
+
}),
|
|
73
|
+
[previewMode],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
77
|
+
const [hasPlaceSelected, setHasPlaceSelected] = useState(false);
|
|
78
|
+
const [selectedCityId, setSelectedCityId] = useState<string | null>(null);
|
|
79
|
+
const [selectedPlaceId, setSelectedPlaceId] = useState<string | null>(null);
|
|
80
|
+
const [placeMeta, setPlaceMeta] = useState<PlaceSelectionMeta | null>(null);
|
|
81
|
+
const [deviceId, setDeviceId] = useState<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const client = useWastoClient();
|
|
84
|
+
|
|
85
|
+
const registerDevice = useCallback(
|
|
86
|
+
async (cityId: string, placeId: string, meta?: PlaceSelectionMeta) => {
|
|
87
|
+
setIsLoading(true);
|
|
88
|
+
const nextMeta: PlaceSelectionMeta = {
|
|
89
|
+
cityName: meta?.cityName?.trim() || null,
|
|
90
|
+
streetName: meta?.streetName?.trim() || null,
|
|
91
|
+
sectorName: meta?.sectorName?.trim() || null,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let resolvedDeviceId: string | null = null;
|
|
95
|
+
if (!previewMode) {
|
|
96
|
+
const newDeviceId = generateDeviceId();
|
|
97
|
+
await client.public.analytics.track({
|
|
98
|
+
id: newDeviceId,
|
|
99
|
+
cityId: cityId,
|
|
100
|
+
platform: Platform.OS,
|
|
101
|
+
osVersion: null,
|
|
102
|
+
appVersion: null,
|
|
103
|
+
deviceModel: null,
|
|
104
|
+
});
|
|
105
|
+
await AsyncStorage.setItem(keys.device, newDeviceId);
|
|
106
|
+
setDeviceId(newDeviceId);
|
|
107
|
+
resolvedDeviceId = newDeviceId;
|
|
108
|
+
} else {
|
|
109
|
+
// Studio preview: never create analytics devices / persist device id.
|
|
110
|
+
setDeviceId(null);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await AsyncStorage.setItem(keys.city, cityId);
|
|
114
|
+
await AsyncStorage.setItem(keys.place, placeId);
|
|
115
|
+
await AsyncStorage.setItem(keys.placeMeta, JSON.stringify(nextMeta));
|
|
116
|
+
|
|
117
|
+
setHasPlaceSelected(true);
|
|
118
|
+
setSelectedCityId(cityId);
|
|
119
|
+
setSelectedPlaceId(placeId);
|
|
120
|
+
setPlaceMeta(nextMeta);
|
|
121
|
+
setIsLoading(false);
|
|
122
|
+
return resolvedDeviceId;
|
|
123
|
+
},
|
|
124
|
+
[client.public.analytics, keys, previewMode],
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const clearPlaceSelection = useCallback(async () => {
|
|
128
|
+
await AsyncStorage.multiRemove([keys.city, keys.place, keys.placeMeta]);
|
|
129
|
+
setHasPlaceSelected(false);
|
|
130
|
+
setSelectedCityId(null);
|
|
131
|
+
setSelectedPlaceId(null);
|
|
132
|
+
setPlaceMeta(null);
|
|
133
|
+
}, [keys]);
|
|
134
|
+
|
|
135
|
+
const clearLocalData = useCallback(async () => {
|
|
136
|
+
await AsyncStorage.multiRemove([
|
|
137
|
+
keys.city,
|
|
138
|
+
keys.place,
|
|
139
|
+
keys.placeMeta,
|
|
140
|
+
keys.device,
|
|
141
|
+
keys.dismissed,
|
|
142
|
+
]);
|
|
143
|
+
setHasPlaceSelected(false);
|
|
144
|
+
setSelectedCityId(null);
|
|
145
|
+
setSelectedPlaceId(null);
|
|
146
|
+
setPlaceMeta(null);
|
|
147
|
+
setDeviceId(null);
|
|
148
|
+
}, [keys]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const loadLocalData = async () => {
|
|
152
|
+
setIsLoading(true);
|
|
153
|
+
|
|
154
|
+
const [
|
|
155
|
+
localSelectedCityId,
|
|
156
|
+
localSelectedPlaceId,
|
|
157
|
+
localPlaceMetaRaw,
|
|
158
|
+
localDeviceId,
|
|
159
|
+
] = await Promise.all([
|
|
160
|
+
AsyncStorage.getItem(keys.city),
|
|
161
|
+
AsyncStorage.getItem(keys.place),
|
|
162
|
+
AsyncStorage.getItem(keys.placeMeta),
|
|
163
|
+
previewMode ? Promise.resolve(null) : AsyncStorage.getItem(keys.device),
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
let localPlaceMeta: PlaceSelectionMeta | null = null;
|
|
167
|
+
if (localPlaceMetaRaw) {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(localPlaceMetaRaw) as PlaceSelectionMeta;
|
|
170
|
+
localPlaceMeta = {
|
|
171
|
+
cityName: parsed.cityName ?? null,
|
|
172
|
+
streetName: parsed.streetName ?? null,
|
|
173
|
+
sectorName: parsed.sectorName ?? null,
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
localPlaceMeta = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const ready = Boolean(localSelectedCityId && localSelectedPlaceId);
|
|
181
|
+
setHasPlaceSelected(ready);
|
|
182
|
+
setSelectedCityId(localSelectedCityId);
|
|
183
|
+
setSelectedPlaceId(localSelectedPlaceId);
|
|
184
|
+
setPlaceMeta(localPlaceMeta);
|
|
185
|
+
setDeviceId(previewMode ? null : localDeviceId);
|
|
186
|
+
|
|
187
|
+
setIsLoading(false);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
void loadLocalData();
|
|
191
|
+
}, [keys, previewMode]);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<LocalContext
|
|
195
|
+
value={{
|
|
196
|
+
isLoading,
|
|
197
|
+
hasPlaceSelected,
|
|
198
|
+
hasCitySelected: hasPlaceSelected,
|
|
199
|
+
selectedCityId,
|
|
200
|
+
selectedPlaceId,
|
|
201
|
+
placeMeta,
|
|
202
|
+
deviceId,
|
|
203
|
+
registerDevice,
|
|
204
|
+
clearPlaceSelection,
|
|
205
|
+
clearLocalData,
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
{children}
|
|
209
|
+
</LocalContext>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function useLocal() {
|
|
214
|
+
const context = useContext(LocalContext);
|
|
215
|
+
if (!context) {
|
|
216
|
+
throw new Error("useLocal must be within a LocalProvider.");
|
|
217
|
+
}
|
|
218
|
+
return context;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function generateDeviceId() {
|
|
222
|
+
const randomCharacters = Array.from({ length: 8 }, () =>
|
|
223
|
+
Math.floor(Math.random() * 10),
|
|
224
|
+
).join("");
|
|
225
|
+
|
|
226
|
+
return `device--${Date.now()}--${Platform.OS}--${randomCharacters}`;
|
|
227
|
+
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
PropsWithChildren,
|
|
4
|
-
useContext,
|
|
5
|
-
useMemo,
|
|
6
|
-
type ReactElement,
|
|
7
|
-
} from "react";
|
|
8
|
-
import { resolveTheme } from "../helpers/themeStyles";
|
|
9
|
-
import type { ResolvedTheme, Theme } from "../types/Theme";
|
|
10
|
-
|
|
11
|
-
export type ThemeContextType = {
|
|
12
|
-
theme: ResolvedTheme;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
16
|
-
|
|
17
|
-
export type ThemePoviderProps = PropsWithChildren & {
|
|
18
|
-
theme: Theme;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function ThemeProvider(props: ThemePoviderProps): ReactElement {
|
|
22
|
-
const theme = useMemo(() => resolveTheme(props.theme), [props.theme]);
|
|
23
|
-
|
|
24
|
-
return <ThemeContext
|
|
25
|
-
value={{ theme }}
|
|
26
|
-
>
|
|
27
|
-
{props.children}
|
|
28
|
-
</ThemeContext>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function useTheme() {
|
|
32
|
-
const context = useContext(ThemeContext);
|
|
33
|
-
if (!context) {
|
|
34
|
-
throw new Error("useTheme must be used within a ThemeProvider.");
|
|
35
|
-
}
|
|
36
|
-
return context;
|
|
37
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
PropsWithChildren,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { resolveTheme } from "../helpers/themeStyles";
|
|
9
|
+
import type { ResolvedTheme, Theme } from "../types/Theme";
|
|
10
|
+
|
|
11
|
+
export type ThemeContextType = {
|
|
12
|
+
theme: ResolvedTheme;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
export type ThemePoviderProps = PropsWithChildren & {
|
|
18
|
+
theme: Theme;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function ThemeProvider(props: ThemePoviderProps): ReactElement {
|
|
22
|
+
const theme = useMemo(() => resolveTheme(props.theme), [props.theme]);
|
|
23
|
+
|
|
24
|
+
return <ThemeContext
|
|
25
|
+
value={{ theme }}
|
|
26
|
+
>
|
|
27
|
+
{props.children}
|
|
28
|
+
</ThemeContext>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useTheme() {
|
|
32
|
+
const context = useContext(ThemeContext);
|
|
33
|
+
if (!context) {
|
|
34
|
+
throw new Error("useTheme must be used within a ThemeProvider.");
|
|
35
|
+
}
|
|
36
|
+
return context;
|
|
37
|
+
}
|