Skip to content

Commit 7fc9141

Browse files
authored
Merge pull request #647 from Telegram-Mini-Apps/feature/location-manager-and-share-message
Feature/location manager and share message
2 parents 059c8af + fbcfd81 commit 7fc9141

File tree

13 files changed

+307
-6
lines changed

13 files changed

+307
-6
lines changed

.changeset/eleven-jobs-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@telegram-apps/bridge": patch
3+
---
4+
5+
Use `Maybe` from `@telegram-apps/toolkit` instead of a type from declarations' file

.changeset/long-candles-confess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@telegram-apps/sdk": minor
3+
---
4+
5+
Implement location manager. Implement `shareMessage`. Add support check to `biometryManager.mount()`.

packages/bridge/src/events/types/events.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
EmojiStatusFailedError,
1414
HomeScreenStatus,
1515
} from './misc.js';
16-
import type { If, IsNever } from '@telegram-apps/toolkit';
16+
import type { If, IsNever, Maybe } from '@telegram-apps/toolkit';
1717

1818
/**
1919
* Map where key is known event name, and value is its listener.

packages/bridge/src/util.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/sdk/src/errors.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,9 @@ export const [
5353
export const [
5454
FullscreenFailedError,
5555
isFullscreenFailedError,
56-
] = errorClass<[message: string]>('FullscreenFailedError', proxyMessage);
56+
] = errorClass<[message: string]>('FullscreenFailedError', proxyMessage);
57+
58+
export const [
59+
ShareMessageError,
60+
isShareMessageError,
61+
] = errorClass<[error: string]>('ShareMessageError', proxyMessage);

packages/sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from '@/scopes/components/cloud-storage/exports.js';
55
export * from '@/scopes/components/haptic-feedback/exports.js';
66
export * from '@/scopes/components/init-data/exports.js';
77
export * from '@/scopes/components/invoice/exports.js';
8+
export * from '@/scopes/components/location-manager/exports.js';
89
export * from '@/scopes/components/main-button/exports.js';
910
export * from '@/scopes/components/mini-app/exports.js';
1011
export * from '@/scopes/components/popup/exports.js';

packages/sdk/src/scopes/components/biometry/methods.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { defineMountFn } from '@/scopes/defineMountFn.js';
1515
import { createIsSupported } from '@/scopes/createIsSupported.js';
1616
import { createWrapComplete } from '@/scopes/wrappers/createWrapComplete.js';
1717
import { createWrapSupported } from '@/scopes/wrappers/createWrapSupported.js';
18-
import { createWrapBasic } from '@/scopes/wrappers/createWrapBasic.js';
1918
import { defineNonConcurrentFn } from '@/scopes/defineNonConcurrentFn.js';
2019
import { NotAvailableError } from '@/errors.js';
2120

@@ -88,7 +87,6 @@ const [
8887
},
8988
);
9089

91-
const wrapBasic = createWrapBasic(COMPONENT_NAME);
9290
const wrapSupported = createWrapSupported(COMPONENT_NAME, REQUEST_AUTH_METHOD);
9391
const wrapComplete = createWrapComplete(COMPONENT_NAME, tIsMounted[0], REQUEST_AUTH_METHOD);
9492

@@ -103,7 +101,7 @@ const wrapComplete = createWrapComplete(COMPONENT_NAME, tIsMounted[0], REQUEST_A
103101
* await mount();
104102
* }
105103
*/
106-
export const mount = wrapBasic('mount', mountFn);
104+
export const mount = wrapSupported('mount', mountFn);
107105
export const [, mountPromise, isMounting] = tMountPromise;
108106
export const [, mountError] = tMountError;
109107
export const [_isMounted, isMounted] = tIsMounted;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export {
2+
type State as LocationManagerState,
3+
isAccessGranted as isLocationManagerAccessGranted,
4+
isAccessRequested as isLocationManagerAccessRequested,
5+
requestLocationPromise,
6+
isRequestingLocation,
7+
requestLocationError,
8+
isMounted as isLocationManagerMounted,
9+
isMounting as isLocationManagerMounting,
10+
mount as mountLocationManager,
11+
mountError as locationManagerMountError,
12+
requestLocation,
13+
mountPromise as locationManagerMountPromise,
14+
isAvailable as isLocationManagerAvailable,
15+
} from './exports.variable.js';
16+
export * as locationManager from './exports.variable.js';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export {
2+
type State,
3+
isAccessGranted,
4+
isAccessRequested,
5+
requestLocationPromise,
6+
isRequestingLocation,
7+
requestLocationError,
8+
isMounted,
9+
isMounting,
10+
mount,
11+
mountError,
12+
requestLocation,
13+
mountPromise,
14+
isAvailable,
15+
} from './location-manager.js';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { isPageReload } from '@telegram-apps/navigation';
2+
import { getStorageValue, Maybe, setStorageValue } from '@telegram-apps/toolkit';
3+
import { AbortablePromise } from 'better-promises';
4+
import type { EventPayload } from '@telegram-apps/bridge';
5+
import type { Computed } from '@telegram-apps/signals';
6+
7+
import { defineMountFn } from '@/scopes/defineMountFn.js';
8+
import { request } from '@/globals.js';
9+
import { createWrapComplete } from '@/scopes/wrappers/createWrapComplete.js';
10+
import { createWrapSupported } from '@/scopes/wrappers/createWrapSupported.js';
11+
import { NotAvailableError } from '@/errors.js';
12+
import { defineNonConcurrentFn } from '@/scopes/defineNonConcurrentFn.js';
13+
import { signalCancel } from '@/scopes/signalCancel.js';
14+
import type { AsyncOptions } from '@/types.js';
15+
import { createComputed, createSignal } from '@/signals-registry.js';
16+
17+
const COMPONENT_NAME = 'locationManager';
18+
const CHECK_LOCATION_METHOD = 'web_app_check_location';
19+
20+
export interface State {
21+
/**
22+
* If true, indicates that location data tracking is available on the current device.
23+
*/
24+
available: boolean;
25+
/**
26+
* Indicates whether the app has previously requested permission to track location data.
27+
*/
28+
accessRequested: boolean;
29+
/**
30+
* Indicates whether the user has granted the app permission to track location data.
31+
*
32+
* If false and `accessRequested` is true may indicate that:
33+
*
34+
* - The user has simply canceled the permission popup.
35+
* - The user has denied the app permission to track location data.
36+
*/
37+
accessGranted: boolean;
38+
}
39+
40+
type StorageValue = State;
41+
42+
const state = createSignal<State>({
43+
available: false,
44+
accessGranted: false,
45+
accessRequested: false,
46+
});
47+
48+
function fromState<K extends keyof State>(key: K): Computed<State[K]> {
49+
return createComputed(() => state()[key]);
50+
}
51+
52+
/**
53+
* Signal indicating whether the location data tracking is currently available.
54+
*/
55+
export const isAvailable = fromState('available');
56+
57+
/**
58+
* Signal indicating whether the user has granted the app permission to track location data.
59+
*/
60+
export const isAccessGranted = fromState('accessGranted');
61+
62+
/**
63+
* Signal indicating whether the app has previously requested permission to track location data.
64+
*/
65+
export const isAccessRequested = fromState('accessRequested');
66+
67+
/**
68+
* Converts `location_checked` to some common shape.
69+
* @param event - event payload.
70+
* @see location_checked
71+
*/
72+
function eventToState(event: EventPayload<'location_checked'>): State {
73+
console.log(event);
74+
let available = false;
75+
let accessRequested: Maybe<boolean>;
76+
let accessGranted: Maybe<boolean>;
77+
if (event.available) {
78+
available = true;
79+
accessRequested = event.access_requested;
80+
accessGranted = event.access_granted;
81+
}
82+
return {
83+
available,
84+
accessGranted: accessGranted || false,
85+
accessRequested: accessRequested || false,
86+
};
87+
}
88+
89+
const [
90+
mountFn,
91+
tMountPromise,
92+
tMountError,
93+
tIsMounted,
94+
] = defineMountFn(
95+
COMPONENT_NAME,
96+
(options?: AsyncOptions) => {
97+
const s = isPageReload() && getStorageValue<StorageValue>(COMPONENT_NAME);
98+
return s
99+
? AbortablePromise.resolve(s)
100+
: request('web_app_check_location', 'location_checked', options).then(eventToState);
101+
},
102+
s => {
103+
state.set(s);
104+
setStorageValue<State>(COMPONENT_NAME, s);
105+
},
106+
);
107+
108+
const wrapSupported = createWrapSupported(COMPONENT_NAME, CHECK_LOCATION_METHOD);
109+
const wrapComplete = createWrapComplete(COMPONENT_NAME, tIsMounted[0], CHECK_LOCATION_METHOD);
110+
111+
/**
112+
* Mounts the location manager component.
113+
* @since Mini Apps v8.0
114+
* @throws {FunctionNotAvailableError} The environment is unknown
115+
* @throws {FunctionNotAvailableError} The SDK is not initialized
116+
* @throws {FunctionNotAvailableError} The function is not supported
117+
* @example
118+
* if (mount.isAvailable()) {
119+
* await mount();
120+
* }
121+
*/
122+
export const mount = wrapSupported('mount', mountFn);
123+
export const [, mountPromise, isMounting] = tMountPromise;
124+
export const [, mountError] = tMountError;
125+
export const [_isMounted, isMounted] = tIsMounted;
126+
127+
const [
128+
reqLocationFn,
129+
tReqLocationPromise,
130+
tReqLocationError,
131+
] = defineNonConcurrentFn(
132+
(options?: AsyncOptions) => {
133+
return request('web_app_request_location', 'location_requested', options).then(data => {
134+
if (!data.available) {
135+
state.set({ ...state(), available: false });
136+
throw new NotAvailableError('Location data tracking is not available');
137+
}
138+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
139+
const { available, ...rest } = data;
140+
return rest;
141+
});
142+
},
143+
'Location request is currently in progress',
144+
);
145+
146+
147+
/**
148+
* Requests location data.
149+
* @since Mini Apps v8.0
150+
* @returns Promise with location data.
151+
* @throws {FunctionNotAvailableError} The environment is unknown
152+
* @throws {FunctionNotAvailableError} The SDK is not initialized
153+
* @throws {FunctionNotAvailableError} The function is not supported
154+
* @throws {FunctionNotAvailableError} The parent component is not mounted
155+
* @throws {ConcurrentCallError} Location request is currently in progress
156+
* @throws {NotAvailableError} Location data tracking is not available
157+
* @example
158+
* if (requestLocation.isAvailable()) {
159+
* const location = await requestLocation();
160+
* }
161+
*/
162+
export const requestLocation = wrapComplete('getLocation', reqLocationFn);
163+
export const [, requestLocationPromise, isRequestingLocation] = tReqLocationPromise;
164+
export const [, requestLocationError] = tReqLocationError;
165+
166+
167+
/**
168+
* Unmounts the component.
169+
*/
170+
export function unmount(): void {
171+
signalCancel(requestLocationPromise);
172+
_isMounted.set(false);
173+
}

0 commit comments

Comments
 (0)