Skip to content

Commit a00daf0

Browse files
committed
Allow to override geolocation providers via Fork API
1 parent 409488f commit a00daf0

File tree

4 files changed

+218
-82
lines changed

4 files changed

+218
-82
lines changed

apps/website/docs/web-api/geolocation.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ import demoFile from './geolocation.live.vue?raw';
5858

5959
## Additional capabilities
6060

61+
While creating an integration, you can override the default geolocation provider with your custom one. It can be done by passing an array of providers to the `trackGeolocation` function.
62+
63+
```ts
64+
import { trackGeolocation } from '@withease/web-api';
65+
66+
const geo = trackGeolocation({
67+
/* ... */
68+
// by default providers field contains trackGeolocation.browserProvider
69+
// which represents the browser built-in Geolocation API
70+
providers: [trackGeolocation.browserProvider],
71+
});
72+
```
73+
74+
The logic is quite straightforward: integration will call providers one by one until one of them returns the location. The first provider that returns the location will be used.
75+
6176
### Regional restrictions
6277

6378
In some countries and regions, the use of geolocation can be restricted. If you are aiming to provide a service in such locations, you use some local providers to get the location of the user. For example, in China, you can use [Baidu](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), [Autonavi](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), or [Tencent](https://lbs.qq.com/webApi/component/componentGuide/componentGeolocation).
@@ -227,3 +242,26 @@ const geo = trackGeolocation({
227242
],
228243
});
229244
```
245+
246+
### Testing
247+
248+
You can pass a [_Store_](https://effector.dev/docs/api/effector/store) to `providers` option to get a way to mock the geolocation provider during testing via [Fork API](/magazine/fork_api_rules).
249+
250+
```ts
251+
import { createStore, fork } from 'effector';
252+
import { trackGeolocation } from '@withease/web-api';
253+
254+
// Create a store with the default provider
255+
const $geolocationProviders = createStore([trackGeolocation.browserProvider]);
256+
257+
// Create an integration with the store
258+
const geo = trackGeolocation({
259+
/* ... */
260+
providers: $geolocationProviders,
261+
});
262+
263+
// during testing, you can replace the provider with your mock
264+
const scope = fork({ values: [[$geolocationProviders, myFakeProvider]] });
265+
```
266+
267+
That is it, any calculations on the created [_Scope_](https://effector.dev/docs/api/effector/scope) will use the `myFakeProvider` instead of the default one.

packages/web-api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
},
88
"scripts": {
99
"test:run": "vitest run --typecheck",
10+
"test:watch": "vitest --typecheck",
1011
"build": "vite build",
1112
"size": "size-limit",
1213
"publint": "node ../../tools/publint.mjs",

packages/web-api/src/geolocation.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*/
55

66
import { allSettled, createStore, createWatch, fork } from 'effector';
7-
import { trackGeolocation } from 'geolocation';
87
import { describe, expect, test, vi } from 'vitest';
98

9+
import { trackGeolocation } from './geolocation';
10+
1011
describe('trackGeolocation', () => {
1112
test('request', async () => {
1213
let lat = 41.890221;
@@ -143,3 +144,56 @@ describe('trackGeolocation', () => {
143144
expect(() => trackGeolocation()).not.toThrow();
144145
});
145146
});
147+
148+
describe('trackGeolocation, providers as a Store', () => {
149+
const firstProvider = () => ({
150+
name: 'firstProvider',
151+
async getCurrentPosition() {
152+
return {
153+
coords: { latitude: 1, longitude: 1 },
154+
timestamp: Date.now(),
155+
};
156+
},
157+
watchPosition(success: any, error: any) {
158+
return () => {};
159+
},
160+
});
161+
162+
const secondProvider = () => ({
163+
name: 'secondProvider',
164+
async getCurrentPosition() {
165+
return {
166+
coords: { latitude: 2, longitude: 2 },
167+
timestamp: Date.now(),
168+
};
169+
},
170+
watchPosition(success: any, error: any) {
171+
return () => {};
172+
},
173+
});
174+
175+
const $providers = createStore([firstProvider]);
176+
177+
const geo = trackGeolocation({ providers: $providers });
178+
179+
test('request', async () => {
180+
const scopeWithOriginal = fork();
181+
const scopeWithReplace = fork({ values: [[$providers, [secondProvider]]] });
182+
183+
await allSettled(geo.request, { scope: scopeWithReplace });
184+
expect(scopeWithReplace.getState(geo.$location)).toMatchInlineSnapshot(`
185+
{
186+
"latitude": 2,
187+
"longitude": 2,
188+
}
189+
`);
190+
191+
await allSettled(geo.request, { scope: scopeWithOriginal });
192+
expect(scopeWithOriginal.getState(geo.$location)).toMatchInlineSnapshot(`
193+
{
194+
"latitude": 1,
195+
"longitude": 1,
196+
}
197+
`);
198+
});
199+
});

packages/web-api/src/geolocation.ts

Lines changed: 124 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
type Event,
33
type EventCallable,
44
type Store,
5+
type Effect,
56
combine,
67
createEvent,
78
createStore,
8-
createEffect,
99
sample,
1010
attach,
1111
scopeBind,
12+
is,
1213
} from 'effector';
1314

1415
import { readonly } from './shared';
@@ -77,37 +78,41 @@ const BrowserProvider = Symbol('BrowserProvider');
7778

7879
export function trackGeolocation(
7980
params?: GeolocationParams & {
80-
providers?: Array<
81-
typeof BrowserProvider | CustomProvider | globalThis.Geolocation
82-
>;
81+
providers?:
82+
| Array<typeof BrowserProvider | CustomProvider | globalThis.Geolocation>
83+
| Store<
84+
Array<
85+
typeof BrowserProvider | CustomProvider | globalThis.Geolocation
86+
>
87+
>;
8388
}
8489
): Geolocation {
85-
const providres = (
86-
params?.providers ?? /* In case of no providers, we will use the default one only */ [
87-
BrowserProvider,
88-
]
89-
)
90-
.map((provider) => {
91-
/* BrowserProvider symbol means usage of navigator.geolocation */
92-
if (provider === BrowserProvider) {
93-
const browserGeolocationAvailable =
94-
globalThis.navigator && 'geolocation' in globalThis.navigator;
95-
if (!browserGeolocationAvailable) {
96-
return null;
97-
}
98-
99-
return globalThis.navigator.geolocation;
100-
}
90+
let $providers: Store<
91+
Array<typeof BrowserProvider | CustomProvider | globalThis.Geolocation>
92+
>;
93+
if (is.store(params?.providers)) {
94+
$providers = params.providers;
95+
} else {
96+
$providers = createStore(params?.providers ?? [BrowserProvider]);
97+
}
10198

102-
if (isDefaultProvider(provider)) {
103-
return provider;
104-
}
99+
const initializeAllProvidersFx = attach({
100+
source: $providers,
101+
effect(providers) {
102+
return providers
103+
.map((provider) => initializeProvider(provider, params))
104+
.filter(Boolean) as Array<
105+
ReturnType<CustomProvider> | globalThis.Geolocation
106+
>;
107+
},
108+
});
105109

106-
return provider(params ?? {});
107-
})
108-
.filter(Boolean) as Array<
110+
const $initializedProviders = createStore<Array<
109111
ReturnType<CustomProvider> | globalThis.Geolocation
110-
>;
112+
> | null>(null, { serialize: 'ignore' }).on(
113+
initializeAllProvidersFx.doneData,
114+
(_, providers) => providers
115+
);
111116

112117
// -- units
113118

@@ -138,6 +143,13 @@ export function trackGeolocation(
138143

139144
// -- shared logic
140145

146+
sample({
147+
clock: [request, startWatching],
148+
source: $initializedProviders,
149+
filter: (providers) => !providers,
150+
target: initializeAllProvidersFx,
151+
});
152+
141153
const newPosition = createEvent<
142154
CustomGeolocationPosition | globalThis.GeolocationPosition
143155
>();
@@ -150,35 +162,38 @@ export function trackGeolocation(
150162

151163
// -- get current position
152164

153-
const getCurrentPositionFx = createEffect<
165+
const getCurrentPositionFx: Effect<
154166
void,
155167
CustomGeolocationPosition | globalThis.GeolocationPosition,
156168
CustomGeolocationError | globalThis.GeolocationPositionError
157-
>(async () => {
158-
let geolocation:
159-
| globalThis.GeolocationPosition
160-
| CustomGeolocationPosition
161-
| null = null;
162-
163-
for (const provider of providres) {
164-
if (isDefaultProvider(provider)) {
165-
geolocation = await new Promise<GeolocationPosition>(
166-
(resolve, rejest) =>
167-
provider.getCurrentPosition(resolve, rejest, params)
168-
);
169-
} else {
170-
geolocation = await provider.getCurrentPosition();
169+
> = attach({
170+
source: $initializedProviders,
171+
async effect(providers) {
172+
let geolocation:
173+
| globalThis.GeolocationPosition
174+
| CustomGeolocationPosition
175+
| null = null;
176+
177+
for (const provider of providers ?? []) {
178+
if (isDefaultProvider(provider)) {
179+
geolocation = await new Promise<GeolocationPosition>(
180+
(resolve, reject) =>
181+
provider.getCurrentPosition(resolve, reject, params)
182+
);
183+
} else {
184+
geolocation = await provider.getCurrentPosition();
185+
}
171186
}
172-
}
173187

174-
if (!geolocation) {
175-
throw {
176-
code: 'POSITION_UNAVAILABLE',
177-
message: 'No avaiable geolocation provider',
178-
};
179-
}
188+
if (!geolocation) {
189+
throw {
190+
code: 'POSITION_UNAVAILABLE',
191+
message: 'No available geolocation provider',
192+
};
193+
}
180194

181-
return geolocation;
195+
return geolocation;
196+
},
182197
});
183198

184199
sample({ clock: request, target: getCurrentPositionFx });
@@ -192,40 +207,46 @@ export function trackGeolocation(
192207

193208
const $unsubscribe = createStore<Unsubscribe | null>(null);
194209

195-
const watchPositionFx = createEffect(() => {
196-
const boundNewPosition = scopeBind(newPosition, { safe: true });
197-
const boundFailed = scopeBind(failed, { safe: true });
198-
199-
const defaultUnwatchMap = new Map<(id: number) => void, number>();
200-
const customUnwatchSet = new Set<Unsubscribe>();
201-
202-
for (const provider of providres) {
203-
if (isDefaultProvider(provider)) {
204-
const watchId = provider.watchPosition(
205-
boundNewPosition,
206-
boundFailed,
207-
params
208-
);
209-
210-
defaultUnwatchMap.set((id: number) => provider.clearWatch(id), watchId);
211-
} else {
212-
const unwatch = provider.watchPosition(boundNewPosition, boundFailed);
213-
214-
customUnwatchSet.add(unwatch);
210+
const watchPositionFx = attach({
211+
source: $initializedProviders,
212+
effect(providers) {
213+
const boundNewPosition = scopeBind(newPosition, { safe: true });
214+
const boundFailed = scopeBind(failed, { safe: true });
215+
216+
const defaultUnwatchMap = new Map<(id: number) => void, number>();
217+
const customUnwatchSet = new Set<Unsubscribe>();
218+
219+
for (const provider of providers ?? []) {
220+
if (isDefaultProvider(provider)) {
221+
const watchId = provider.watchPosition(
222+
boundNewPosition,
223+
boundFailed,
224+
params
225+
);
226+
227+
defaultUnwatchMap.set(
228+
(id: number) => provider.clearWatch(id),
229+
watchId
230+
);
231+
} else {
232+
const unwatch = provider.watchPosition(boundNewPosition, boundFailed);
233+
234+
customUnwatchSet.add(unwatch);
235+
}
215236
}
216-
}
217237

218-
return () => {
219-
for (const [unwatch, id] of defaultUnwatchMap) {
220-
unwatch(id);
221-
defaultUnwatchMap.delete(unwatch);
222-
}
238+
return () => {
239+
for (const [unwatch, id] of defaultUnwatchMap) {
240+
unwatch(id);
241+
defaultUnwatchMap.delete(unwatch);
242+
}
223243

224-
for (const unwatch of customUnwatchSet) {
225-
unwatch();
226-
customUnwatchSet.delete(unwatch);
227-
}
228-
};
244+
for (const unwatch of customUnwatchSet) {
245+
unwatch();
246+
customUnwatchSet.delete(unwatch);
247+
}
248+
};
249+
},
229250
});
230251

231252
const unwatchPositionFx = attach({
@@ -262,6 +283,28 @@ export function trackGeolocation(
262283

263284
trackGeolocation.browserProvider = BrowserProvider;
264285

286+
function initializeProvider(
287+
provider: typeof BrowserProvider | CustomProvider | globalThis.Geolocation,
288+
params?: GeolocationParams
289+
) {
290+
/* BrowserProvider symbol means usage of navigator.geolocation */
291+
if (provider === BrowserProvider) {
292+
const browserGeolocationAvailable =
293+
globalThis.navigator && 'geolocation' in globalThis.navigator;
294+
if (!browserGeolocationAvailable) {
295+
return null;
296+
}
297+
298+
return globalThis.navigator.geolocation;
299+
}
300+
301+
if (isDefaultProvider(provider)) {
302+
return provider;
303+
}
304+
305+
return provider(params ?? {});
306+
}
307+
265308
function isDefaultProvider(provider: any): provider is globalThis.Geolocation {
266309
return (
267310
'getCurrentPosition' in provider &&

0 commit comments

Comments
 (0)