Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geolocation API #84

Merged
merged 37 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
425a46e
Add docs draft
igorkamyshev Apr 17, 2024
f71882c
mark about options
igorkamyshev Apr 17, 2024
4afcece
reporting
igorkamyshev Apr 17, 2024
6b0a7e7
No setup
igorkamyshev Apr 17, 2024
18948c0
Add subtitle
igorkamyshev Apr 17, 2024
13f4a87
Merge branch 'master' into geolocation
igorkamyshev Apr 17, 2024
f882f88
Add `tractGeolocation` integration
igorkamyshev Apr 17, 2024
bb6e629
Merge branch 'geolocation' of github.com:igorkamyshev/withease into g…
igorkamyshev Apr 17, 2024
a28143f
Change API a bit
igorkamyshev Apr 24, 2024
aba6289
Fix typo
igorkamyshev Apr 24, 2024
e53bf27
Add types
igorkamyshev Apr 24, 2024
9a0402e
Add watching back, improve custom providers
igorkamyshev Apr 24, 2024
574c4ca
Add a note about priority
igorkamyshev Apr 24, 2024
19030c0
hide Baidu example
igorkamyshev Apr 24, 2024
c82639f
Allow to exclude browser geolocation from providers
igorkamyshev Apr 24, 2024
6af1cf6
update types
igorkamyshev Apr 24, 2024
3a5d29d
Add browserProvider
igorkamyshev Apr 24, 2024
7a48f6b
Fix typo
igorkamyshev Apr 24, 2024
a79d608
Add real units
igorkamyshev Apr 24, 2024
02b964b
Add basic relations
igorkamyshev Apr 24, 2024
e411ae0
Add basic geolocation request
igorkamyshev Apr 25, 2024
ebaf2d8
Add basic geolocation watching
igorkamyshev Apr 25, 2024
0366dcc
Refactoring
igorkamyshev Apr 25, 2024
137b48e
Add custom request and watch
igorkamyshev Apr 25, 2024
08e9937
Size limit
igorkamyshev Apr 25, 2024
82993b4
Add live demo
igorkamyshev Apr 25, 2024
17bd97b
Final touches
igorkamyshev Apr 29, 2024
98bb6ff
Size
igorkamyshev Apr 29, 2024
29f452a
Merge branch 'master' into geolocation
igorkamyshev Apr 29, 2024
347f78e
Fix usage of external default providers
igorkamyshev Apr 29, 2024
2ff825e
Merge branch 'geolocation' of github.com:igorkamyshev/withease into g…
igorkamyshev Apr 29, 2024
76386a6
Merge branch 'master' into geolocation
igorkamyshev Aug 1, 2024
409488f
Merge branch 'master' into geolocation
igorkamyshev Aug 1, 2024
a00daf0
Allow to override geolocation providers via Fork API
igorkamyshev Aug 1, 2024
fce73a4
Add correct safe calls
igorkamyshev Aug 1, 2024
4fd7080
Build fixes
igorkamyshev Aug 1, 2024
bca44eb
Get red of race condition in old effector
igorkamyshev Aug 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dry-cats-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@withease/web-api': minor
---

Add `trackGeolocation` integration
25 changes: 25 additions & 0 deletions apps/web-api-demo/geolocation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>web-api demo</title>
<base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<section>
<h2>Geolocation</h2>

<p>latitude: <span id="latitude" /></p>
<p>longitude: <span id="longitude" /></p>

<button id="get-location">Get Location</button>

<button id="start-watching">Start Watching</button>
<button id="stop-watching">Stop Watching</button>
</section>
<script type="module" src="/src/geolocation.ts"></script>
</body>
</html>
1 change: 1 addition & 0 deletions apps/web-api-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h1>@withease/web-api</h1>
<li><a href="/page-visibility.html">page-visibility</a></li>
<li><a href="/screen-orientation.html">screen-orientation</a></li>
<li><a href="/preferred-languages.html">preferred-languages</a></li>
<li><a href="/geolocation.html">geolocation</a></li>
</ul>
</body>
</html>
22 changes: 22 additions & 0 deletions apps/web-api-demo/src/geolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { trackGeolocation } from '@withease/web-api';

const latitudeElement = document.querySelector('#latitude')!;
const longitudeElement = document.querySelector('#longitude')!;
const getLocationButton = document.querySelector('#get-location')!;
const startWatchingButton = document.querySelector('#start-watching')!;
const stopWatchingButton = document.querySelector('#stop-watching')!;

const { $latitude, $longitude, request, watching } = trackGeolocation({});

$latitude.watch((latitude) => {
console.log('latitude', latitude);
latitudeElement.textContent = JSON.stringify(latitude);
});
$longitude.watch((longitude) => {
console.log('longitude', longitude);
longitudeElement.textContent = JSON.stringify(longitude);
});

getLocationButton.addEventListener('click', () => request());
startWatchingButton.addEventListener('click', () => watching.start());
stopWatchingButton.addEventListener('click', () => watching.stop());
63 changes: 63 additions & 0 deletions apps/web-api-demo/test/geolocation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, test } from '@playwright/test';

const GEOLOCATION_PAGE = '/geolocation.html';

test.use({
geolocation: { longitude: 41.890221, latitude: 12.492348 },
permissions: ['geolocation'],
});

test('request', async ({ page, context }) => {
await page.goto(GEOLOCATION_PAGE);

const latitudeContainer = await page.$('#latitude');
const longitudeContainer = await page.$('#longitude');
const getLocationButton = await page.$('#get-location');

// By default it should be null
expect(await latitudeContainer!.textContent()).toBe('null');
expect(await longitudeContainer!.textContent()).toBe('null');

// After requesting the location, it should be updated
await getLocationButton!.click();
expect(await latitudeContainer!.textContent()).toBe('12.492348');
expect(await longitudeContainer!.textContent()).toBe('41.890221');

// Change geolocation, values should NOT be updated
await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 });
expect(await latitudeContainer!.textContent()).toBe('12.492348');
expect(await longitudeContainer!.textContent()).toBe('41.890221');
// Request the location again, values should be updated
await getLocationButton!.click();
expect(await latitudeContainer!.textContent()).toBe('32.890221');
expect(await longitudeContainer!.textContent()).toBe('22.492348');
});

test('watch', async ({ page, context }) => {
await page.goto(GEOLOCATION_PAGE);

const latitudeContainer = await page.$('#latitude');
const longitudeContainer = await page.$('#longitude');
const startWatchingButton = await page.$('#start-watching');
const stopWatchingButton = await page.$('#stop-watching');

// By default it should be null
expect(await latitudeContainer!.textContent()).toBe('null');
expect(await longitudeContainer!.textContent()).toBe('null');

// After watching the location, it should be updated immediately
await startWatchingButton!.click();
expect(await latitudeContainer!.textContent()).toBe('12.492348');
expect(await longitudeContainer!.textContent()).toBe('41.890221');

// Change geolocation, values should be updated immediately
await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 });
expect(await latitudeContainer!.textContent()).toBe('32.890221');
expect(await longitudeContainer!.textContent()).toBe('22.492348');

// Stop watching and change geolocation, values should NOT be updated
await stopWatchingButton!.click();
await context.setGeolocation({ longitude: 42.492348, latitude: 52.890221 });
expect(await latitudeContainer!.textContent()).toBe('32.890221');
expect(await longitudeContainer!.textContent()).toBe('22.492348');
});
4 changes: 4 additions & 0 deletions apps/website/docs/.vitepress/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export default defineConfig({
text: 'Preferred languages',
link: '/web-api/preferred_languages',
},
{
text: 'Geolocation',
link: '/web-api/geolocation',
},
],
},
]),
Expand Down
19 changes: 19 additions & 0 deletions apps/website/docs/web-api/geolocation.live.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup>
import { trackGeolocation } from '@withease/web-api';
import { useUnit } from 'effector-vue/composition';

const geo = trackGeolocation();

const { latitude, longitude, request } = useUnit({
latitude: geo.$latitude,
longitude: geo.$longitude,
request: geo.request,
});
</script>

<template>
<p>latitude: {{ latitude }}</p>
<p>longitude: {{ longitude }}</p>

<button v-on:click="request">Request geolocation</button>
</template>
229 changes: 229 additions & 0 deletions apps/website/docs/web-api/geolocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
---
title: Geolocation

outline: [2, 3]
---

# Geolocation <Badge text="since v1.3.0" />

Allows tracking geolocation with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store).

::: info

Uses [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) under the hood

:::

## Usage

All you need to do is to create an integration by calling `trackGeolocation` with an integration options:

- `maximumAge?`: a positive `number` representing the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to `0`, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to `Infinity` the device must return a cached position regardless of its age.
- `timeout?`: a positive `number` representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. The maximum value for this attribute is `Infinity`.
- `enableHighAccuracy?`: a `boolean` that indicates the application would like to receive the best possible results.

```ts
import { trackGeolocation } from '@withease/web-api';

const { $location, $latitude, $longitude, request, reporting, watching } =
trackGeolocation({
maximumAge,
timeout,
enableHighAccuracy,
});
```

Returns an object with:

- `$location` - [_Store_](https://effector.dev/docs/api/effector/store) with the current location in the format `{ latitude, longitude }`
- `$latitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current latitude
- `$longitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current longitude
- `request` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to get the current location
- `watching` - an object with the following properties:
- `start` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to start watching the current location
- `stop` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to stop watching the current location
- `$active` - [_Store_](https://effector.dev/docs/api/effector/store) with `true` if watching is started and `false` if watching is stopped
- `reporting` - an object with the following properties:
- `failed` - [_Event_](https://effector.dev/en/api/effector/event) that fires when the location request fails

## Live demo

Let us show you a live demo of how it works. The following demo displays `$latitude` and `$longitude` values. _Click "Request geolocation" button to retrieve it._

<script setup lang="ts">
import demoFile from './geolocation.live.vue?raw';
</script>

<LiveDemo :demoFile="demoFile" />

## Additional capabilities

### Regional restrictions

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).

Geolocation integration of `@withease/web-api` allows to use any provider additionally to the default one provided by the browser. To do so, you need to pass an `providers` option to the `trackGeolocation` function.

```ts
import { trackGeolocation } from '@withease/web-api';

const geo = trackGeolocation({
/* ... */
providers: [
igorkamyshev marked this conversation as resolved.
Show resolved Hide resolved
/* default browser Geolocation API */
trackGeolocation.browserProvider,
/* your custom providers */
],
});
```

Any provider should conform to the following contract:

```ts
/* This type mimics https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition */
type CustomGeolocationPosition = {
timestamp: number;
coords: {
latitude: number;
longitude: number;
accuracy?: number;
altitude?: number;
altitudeAccuracy?: number;
heading?: number;
speed?: number;
};
};

/* This type mimics https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError */
type CustomGeolocationError = {
/*
* You have to map your error codes to the Geolocation API error codes
* In case of unknown error, you are free to skip this field
*/
code?: 'PERMISSION_DENIED' | 'POSITION_UNAVAILABLE' | 'TIMEOUT';
/*
* You can provide a custom message for the error
*/
message?: string;
/*
* You can provide a raw error object from your provider
*/
raw?: unknown;
};

type CustomProvider = (
/* All options would be passed from trackGeolocation call */ {
maximumAge,
timeout,
enableHighAccuracy,
}
) => {
/* This function can throw CustomGeolocationError in case of error */
getCurrentPosition: () => Promise<CustomGeolocationPosition>;
/*
* This function should call successCallback with the position or errorCallback with the error.
* Function should return an Unsubscribe function, which should stop watching the position.
*/
watchPosition: (
successCallback: (position: CustomGeolocationPosition) => void,
errorCallback: (error: CustomGeolocationError) => void
) => Unsubscribe;
};
```

::: details Baidu example

For example, in case of Baidu, you can write something like this:

```ts
function baiduProvider({ maximumAge, timeout, enableHighAccuracy }) {
// Create a Baidu geolocation instance outside of the getCurrentPosition function
// to avoid creating a new instance every time the function is called
const geolocation = new BMap.Geolocation();

const getCurrentPosition = ({ maximumAge, timeout, enableHighAccuracy }) => {
// getCurrentPosition function should return a Promise
return new Promise((resolve, reject) => {
geolocation.getCurrentPosition(function (r) {
if (this.getStatus() === BMAP_STATUS_SUCCESS) {
// in case of success, resolve with the result
resolve({
timestamp: Date.now(),
coords: { latitude: r.point.lat, longitude: r.point.lng },
});
} else {
// map Baidu error codes to the Geolocation API error codes
let code;
switch (this.getStatus()) {
case BMAP_STATUS_PERMISSION_DENIED:
code = 'PERMISSION_DENIED';
break;
case BMAP_STATUS_SERVICE_UNAVAILABLE:
code = 'POSITION_UNAVAILABLE';
break;
case BMAP_STATUS_TIMEOUT:
code = 'TIMEOUT';
break;
}

// reject with the error object
reject({ code, raw: this.getStatus() });
}
});
});
};

/*
* Bailu does not support watching the position
* so, we have to write an imitation of the watchPosition function
*/
const watchPosition = (successCallback, errorCallback) => {
const timerId = setInterval(async () => {
try {
const position = await getCurrentPosition();

successCallback(position);
} catch (error) {
errorCallback(error);
}
}, 1_000);

return () => clearInterval(timerId);
};

return {
getCurrentPosition,
watchPosition,
};
}

const geo = trackGeolocation({
/* ... */
providers: [
/* default browser Geolocation API */
trackGeolocation.browserProvider,
/* Baidu provider */
baiduProvider,
],
});
```

:::

Array of `providers` would be used in the order they are passed to the `trackGeolocation` function. The first provider that returns the coordinates would be used.

### React Native

In case of React Native, it is recommended to use the [`@react-native-community/geolocation`](https://github.com/michalchudziak/react-native-geolocation) package and do not use `navigator.geolocation` directly. You can easily achieve this by excluding `trackGeolocation.browserProvider` from the list of providers.

```ts
import ReactNativeGeolocation from '@react-native-community/geolocation';

const geo = trackGeolocation({
/* ... */
providers: [
trackGeolocation.browserProvider, // [!code --]
ReactNativeGeolocation, // [!code ++]
],
});
```
2 changes: 1 addition & 1 deletion packages/web-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"size-limit": [
{
"path": "./dist/web-api.js",
"limit": "1.58 kB"
"limit": "2.38 kB"
}
]
}
Loading
Loading