@webority/mobile-core 0.0.23 → 0.0.24
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 +10 -0
- package/lib/commonjs/push/index.js +43 -0
- package/lib/commonjs/push/push.js +280 -0
- package/lib/module/push/index.js +4 -0
- package/lib/module/push/push.js +271 -0
- package/lib/typescript/commonjs/push/index.d.ts +3 -0
- package/lib/typescript/commonjs/push/push.d.ts +117 -0
- package/lib/typescript/module/push/index.d.ts +3 -0
- package/lib/typescript/module/push/push.d.ts +117 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -29,11 +29,21 @@ import { Geolocation } from '@webority/mobile-core/geolocation';
|
|
|
29
29
|
import { openDatabase } from '@webority/mobile-core/sqlite';
|
|
30
30
|
import { downloadFile } from '@webority/mobile-core/download';
|
|
31
31
|
import { shareFile } from '@webority/mobile-core/share';
|
|
32
|
+
import { initPush, enablePush, usePush } from '@webority/mobile-core/push';
|
|
32
33
|
import { compressImage } from '@webority/mobile-core/compressor';
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
Metro resolves requires statically, so anything reachable from the main barrel is bundled for
|
|
36
37
|
every consumer. Keeping these off it is what makes their peers genuinely optional.
|
|
37
38
|
|
|
39
|
+
`/push` registers the phone for push notifications. It needs `expo-notifications`, an optional
|
|
40
|
+
peer, and reports `unavailable` without it or in Expo Go. Call `initPush` once at startup with
|
|
41
|
+
`getInstallationId`, `register`, `unregister` and `onOpen`; `enablePush` asks permission, sends
|
|
42
|
+
the native token (`Fcm` on Android, `Apns` on iOS) and the bundle id to `register`, and
|
|
43
|
+
re-registers when the token changes; `disablePush` calls `unregister`. `onOpen` receives the
|
|
44
|
+
tapped notification's data as strings, including the tap that launched the app. `usePush` gives a
|
|
45
|
+
settings switch its `status` and `enabled`. The library does not remember the switch: an app that
|
|
46
|
+
keeps push on calls `enablePush` at each start.
|
|
47
|
+
|
|
38
48
|
Storage and network status stay on the main barrel but load no package on their own. Wire them at
|
|
39
49
|
startup with `setStorageImplementation` and `setNetworkStatusImplementation` if you want them.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "disablePush", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _push.disablePush;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "enablePush", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _push.enablePush;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "initPush", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _push.initPush;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "setBadgeCount", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _push.setBadgeCount;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "setPushImplementation", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _push.setPushImplementation;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(exports, "usePush", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () {
|
|
39
|
+
return _push.usePush;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
var _push = require("./push.js");
|
|
43
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.usePush = exports.setPushImplementation = exports.setBadgeCount = exports.initPush = exports.enablePush = exports.disablePush = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _index = require("../logger/index.js");
|
|
10
|
+
/** The slice of expo-notifications this module calls, so the optional peer stays untyped at build. */
|
|
11
|
+
|
|
12
|
+
let override = null;
|
|
13
|
+
|
|
14
|
+
/** Expo modules resolved once; null when push cannot work in this build. */
|
|
15
|
+
let resolved;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Replace the native modules. For tests and the showcase, to reach paths a
|
|
19
|
+
* real device never takes; an injected backend is taken on trust, Expo Go
|
|
20
|
+
* included. Pass `null` to restore expo-notifications.
|
|
21
|
+
*/
|
|
22
|
+
const setPushImplementation = impl => {
|
|
23
|
+
override = impl;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The only place expo-notifications and expo-application are named. Expo Go is
|
|
28
|
+
* checked first because expo-notifications refuses remote push there and its
|
|
29
|
+
* import throws on Android.
|
|
30
|
+
*/
|
|
31
|
+
exports.setPushImplementation = setPushImplementation;
|
|
32
|
+
const resolveModules = () => {
|
|
33
|
+
if (override) {
|
|
34
|
+
return override;
|
|
35
|
+
}
|
|
36
|
+
if (resolved !== undefined) {
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const expo = require('expo');
|
|
41
|
+
if (expo.isRunningInExpoGo()) {
|
|
42
|
+
resolved = null;
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
const notifications = require('expo-notifications');
|
|
46
|
+
const application = require('expo-application');
|
|
47
|
+
resolved = {
|
|
48
|
+
notifications,
|
|
49
|
+
applicationId: application.applicationId
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
_index.Logger.warn('[push] expo-notifications is not available; push is off', error);
|
|
53
|
+
resolved = null;
|
|
54
|
+
}
|
|
55
|
+
return resolved;
|
|
56
|
+
};
|
|
57
|
+
let state = {
|
|
58
|
+
status: 'unknown',
|
|
59
|
+
enabled: false
|
|
60
|
+
};
|
|
61
|
+
const subscribers = new Set();
|
|
62
|
+
const setState = next => {
|
|
63
|
+
state = {
|
|
64
|
+
...state,
|
|
65
|
+
...next
|
|
66
|
+
};
|
|
67
|
+
for (const notify of subscribers) {
|
|
68
|
+
notify();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
let config = null;
|
|
72
|
+
let channelsReady = Promise.resolve();
|
|
73
|
+
let tokenSubscription = null;
|
|
74
|
+
let lastOpenedId = null;
|
|
75
|
+
const requireConfig = () => {
|
|
76
|
+
if (!config) {
|
|
77
|
+
throw new Error('[@webority/mobile-core] call initPush before enablePush or disablePush.');
|
|
78
|
+
}
|
|
79
|
+
return config;
|
|
80
|
+
};
|
|
81
|
+
const toPermissionStatus = response => {
|
|
82
|
+
if (response.granted) {
|
|
83
|
+
return 'granted';
|
|
84
|
+
}
|
|
85
|
+
return response.status === 'denied' ? 'denied' : 'unknown';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Push data travels as strings; iOS can hand back numbers or objects for top-level keys. */
|
|
89
|
+
const toStringData = data => {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, value] of Object.entries(data ?? {})) {
|
|
92
|
+
if (value === null || value === undefined) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
out[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
const handleResponse = response => {
|
|
100
|
+
if (!response || !config) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// The cold-start read and the listener can both report the launching tap.
|
|
104
|
+
const id = `${response.notification.request.identifier}:${response.actionIdentifier}`;
|
|
105
|
+
if (id === lastOpenedId) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
lastOpenedId = id;
|
|
109
|
+
try {
|
|
110
|
+
config.onOpen(toStringData(response.notification.request.content.data));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
_index.Logger.error('[push] onOpen threw', error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const createChannels = async (m, channels) => {
|
|
116
|
+
for (const channel of channels) {
|
|
117
|
+
try {
|
|
118
|
+
await m.setNotificationChannelAsync(channel.id, {
|
|
119
|
+
name: channel.name,
|
|
120
|
+
importance: channel.importance === 'high' ? m.AndroidImportance.HIGH : m.AndroidImportance.DEFAULT
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
_index.Logger.error(`[push] creating channel ${channel.id} failed`, error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const toDevice = async (token, applicationId) => {
|
|
128
|
+
const platform = token.type === 'android' ? 'Fcm' : token.type === 'ios' ? 'Apns' : null;
|
|
129
|
+
if (!platform || typeof token.data !== 'string') {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (!applicationId) {
|
|
133
|
+
throw new Error('[@webority/mobile-core] push needs the app bundle id from expo-application.');
|
|
134
|
+
}
|
|
135
|
+
const installationId = await requireConfig().getInstallationId();
|
|
136
|
+
return {
|
|
137
|
+
installationId,
|
|
138
|
+
platform,
|
|
139
|
+
token: token.data,
|
|
140
|
+
appId: applicationId
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Wires push at startup: the foreground handler, Android channels, and tap
|
|
146
|
+
* handling, including the tap that launched the app. Calling it again only
|
|
147
|
+
* replaces the config.
|
|
148
|
+
*/
|
|
149
|
+
const initPush = next => {
|
|
150
|
+
const first = config === null;
|
|
151
|
+
config = next;
|
|
152
|
+
if (!first) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const mods = resolveModules();
|
|
156
|
+
if (!mods) {
|
|
157
|
+
setState({
|
|
158
|
+
status: 'unavailable'
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const m = mods.notifications;
|
|
163
|
+
m.setNotificationHandler({
|
|
164
|
+
handleNotification: async () => {
|
|
165
|
+
const show = config?.showInForeground?.() ?? true;
|
|
166
|
+
return {
|
|
167
|
+
shouldShowBanner: show,
|
|
168
|
+
shouldShowList: show,
|
|
169
|
+
shouldPlaySound: show,
|
|
170
|
+
shouldSetBadge: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
if (_reactNative.Platform.OS === 'android' && next.androidChannels?.length) {
|
|
175
|
+
channelsReady = createChannels(m, next.androidChannels);
|
|
176
|
+
}
|
|
177
|
+
m.addNotificationResponseReceivedListener(handleResponse);
|
|
178
|
+
void m.getLastNotificationResponseAsync().then(response => {
|
|
179
|
+
handleResponse(response);
|
|
180
|
+
return m.clearLastNotificationResponseAsync?.();
|
|
181
|
+
}).catch(error => _index.Logger.error('[push] reading the launch notification failed', error));
|
|
182
|
+
void m.getPermissionsAsync().then(response => setState({
|
|
183
|
+
status: toPermissionStatus(response)
|
|
184
|
+
})).catch(error => _index.Logger.error('[push] permission check failed', error));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Asks for permission, reads the native device token and registers it through
|
|
189
|
+
* `config.register`, then re-registers whenever the OS rotates the token.
|
|
190
|
+
*/
|
|
191
|
+
exports.initPush = initPush;
|
|
192
|
+
const enablePush = async () => {
|
|
193
|
+
requireConfig();
|
|
194
|
+
const mods = resolveModules();
|
|
195
|
+
if (!mods) {
|
|
196
|
+
setState({
|
|
197
|
+
status: 'unavailable'
|
|
198
|
+
});
|
|
199
|
+
return 'unavailable';
|
|
200
|
+
}
|
|
201
|
+
const m = mods.notifications;
|
|
202
|
+
|
|
203
|
+
// Android 13 shows the permission prompt only once a channel exists.
|
|
204
|
+
await channelsReady;
|
|
205
|
+
let permission = await m.getPermissionsAsync();
|
|
206
|
+
if (!permission.granted) {
|
|
207
|
+
permission = await m.requestPermissionsAsync();
|
|
208
|
+
}
|
|
209
|
+
if (!permission.granted) {
|
|
210
|
+
setState({
|
|
211
|
+
status: 'denied'
|
|
212
|
+
});
|
|
213
|
+
return 'denied';
|
|
214
|
+
}
|
|
215
|
+
const device = await toDevice(await m.getDevicePushTokenAsync(), mods.applicationId);
|
|
216
|
+
if (!device) {
|
|
217
|
+
setState({
|
|
218
|
+
status: 'unavailable'
|
|
219
|
+
});
|
|
220
|
+
return 'unavailable';
|
|
221
|
+
}
|
|
222
|
+
await requireConfig().register(device);
|
|
223
|
+
tokenSubscription?.remove();
|
|
224
|
+
tokenSubscription = m.addPushTokenListener(token => {
|
|
225
|
+
void toDevice(token, mods.applicationId).then(next => next ? requireConfig().register(next) : undefined).catch(error => _index.Logger.error('[push] re-registering a new token failed', error));
|
|
226
|
+
});
|
|
227
|
+
setState({
|
|
228
|
+
status: 'granted',
|
|
229
|
+
enabled: true
|
|
230
|
+
});
|
|
231
|
+
return 'granted';
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Unregisters the device through `config.unregister` and stops following token
|
|
236
|
+
* changes. Taps on notifications already delivered still reach `onOpen`.
|
|
237
|
+
*/
|
|
238
|
+
exports.enablePush = enablePush;
|
|
239
|
+
const disablePush = async () => {
|
|
240
|
+
const current = requireConfig();
|
|
241
|
+
await current.unregister(await current.getInstallationId());
|
|
242
|
+
tokenSubscription?.remove();
|
|
243
|
+
tokenSubscription = null;
|
|
244
|
+
setState({
|
|
245
|
+
enabled: false
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
250
|
+
exports.disablePush = disablePush;
|
|
251
|
+
const setBadgeCount = async count => {
|
|
252
|
+
const mods = resolveModules();
|
|
253
|
+
if (!mods) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
await mods.notifications.setBadgeCountAsync(count);
|
|
257
|
+
};
|
|
258
|
+
exports.setBadgeCount = setBadgeCount;
|
|
259
|
+
const subscribe = notify => {
|
|
260
|
+
subscribers.add(notify);
|
|
261
|
+
return () => {
|
|
262
|
+
subscribers.delete(notify);
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
const getSnapshot = () => state;
|
|
266
|
+
const enable = async () => {
|
|
267
|
+
await enablePush();
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/** Push status for a settings switch, re-rendering when it changes. */
|
|
271
|
+
const usePush = () => {
|
|
272
|
+
const snapshot = (0, _react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
273
|
+
return {
|
|
274
|
+
...snapshot,
|
|
275
|
+
enable,
|
|
276
|
+
disable: disablePush
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
exports.usePush = usePush;
|
|
280
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import { Platform } from 'react-native';
|
|
5
|
+
import { Logger } from "../logger/index.js";
|
|
6
|
+
|
|
7
|
+
/** The slice of expo-notifications this module calls, so the optional peer stays untyped at build. */
|
|
8
|
+
|
|
9
|
+
let override = null;
|
|
10
|
+
|
|
11
|
+
/** Expo modules resolved once; null when push cannot work in this build. */
|
|
12
|
+
let resolved;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replace the native modules. For tests and the showcase, to reach paths a
|
|
16
|
+
* real device never takes; an injected backend is taken on trust, Expo Go
|
|
17
|
+
* included. Pass `null` to restore expo-notifications.
|
|
18
|
+
*/
|
|
19
|
+
export const setPushImplementation = impl => {
|
|
20
|
+
override = impl;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The only place expo-notifications and expo-application are named. Expo Go is
|
|
25
|
+
* checked first because expo-notifications refuses remote push there and its
|
|
26
|
+
* import throws on Android.
|
|
27
|
+
*/
|
|
28
|
+
const resolveModules = () => {
|
|
29
|
+
if (override) {
|
|
30
|
+
return override;
|
|
31
|
+
}
|
|
32
|
+
if (resolved !== undefined) {
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const expo = require('expo');
|
|
37
|
+
if (expo.isRunningInExpoGo()) {
|
|
38
|
+
resolved = null;
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
const notifications = require('expo-notifications');
|
|
42
|
+
const application = require('expo-application');
|
|
43
|
+
resolved = {
|
|
44
|
+
notifications,
|
|
45
|
+
applicationId: application.applicationId
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
Logger.warn('[push] expo-notifications is not available; push is off', error);
|
|
49
|
+
resolved = null;
|
|
50
|
+
}
|
|
51
|
+
return resolved;
|
|
52
|
+
};
|
|
53
|
+
let state = {
|
|
54
|
+
status: 'unknown',
|
|
55
|
+
enabled: false
|
|
56
|
+
};
|
|
57
|
+
const subscribers = new Set();
|
|
58
|
+
const setState = next => {
|
|
59
|
+
state = {
|
|
60
|
+
...state,
|
|
61
|
+
...next
|
|
62
|
+
};
|
|
63
|
+
for (const notify of subscribers) {
|
|
64
|
+
notify();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
let config = null;
|
|
68
|
+
let channelsReady = Promise.resolve();
|
|
69
|
+
let tokenSubscription = null;
|
|
70
|
+
let lastOpenedId = null;
|
|
71
|
+
const requireConfig = () => {
|
|
72
|
+
if (!config) {
|
|
73
|
+
throw new Error('[@webority/mobile-core] call initPush before enablePush or disablePush.');
|
|
74
|
+
}
|
|
75
|
+
return config;
|
|
76
|
+
};
|
|
77
|
+
const toPermissionStatus = response => {
|
|
78
|
+
if (response.granted) {
|
|
79
|
+
return 'granted';
|
|
80
|
+
}
|
|
81
|
+
return response.status === 'denied' ? 'denied' : 'unknown';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Push data travels as strings; iOS can hand back numbers or objects for top-level keys. */
|
|
85
|
+
const toStringData = data => {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [key, value] of Object.entries(data ?? {})) {
|
|
88
|
+
if (value === null || value === undefined) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
out[key] = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
const handleResponse = response => {
|
|
96
|
+
if (!response || !config) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// The cold-start read and the listener can both report the launching tap.
|
|
100
|
+
const id = `${response.notification.request.identifier}:${response.actionIdentifier}`;
|
|
101
|
+
if (id === lastOpenedId) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
lastOpenedId = id;
|
|
105
|
+
try {
|
|
106
|
+
config.onOpen(toStringData(response.notification.request.content.data));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Logger.error('[push] onOpen threw', error);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const createChannels = async (m, channels) => {
|
|
112
|
+
for (const channel of channels) {
|
|
113
|
+
try {
|
|
114
|
+
await m.setNotificationChannelAsync(channel.id, {
|
|
115
|
+
name: channel.name,
|
|
116
|
+
importance: channel.importance === 'high' ? m.AndroidImportance.HIGH : m.AndroidImportance.DEFAULT
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
Logger.error(`[push] creating channel ${channel.id} failed`, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const toDevice = async (token, applicationId) => {
|
|
124
|
+
const platform = token.type === 'android' ? 'Fcm' : token.type === 'ios' ? 'Apns' : null;
|
|
125
|
+
if (!platform || typeof token.data !== 'string') {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!applicationId) {
|
|
129
|
+
throw new Error('[@webority/mobile-core] push needs the app bundle id from expo-application.');
|
|
130
|
+
}
|
|
131
|
+
const installationId = await requireConfig().getInstallationId();
|
|
132
|
+
return {
|
|
133
|
+
installationId,
|
|
134
|
+
platform,
|
|
135
|
+
token: token.data,
|
|
136
|
+
appId: applicationId
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Wires push at startup: the foreground handler, Android channels, and tap
|
|
142
|
+
* handling, including the tap that launched the app. Calling it again only
|
|
143
|
+
* replaces the config.
|
|
144
|
+
*/
|
|
145
|
+
export const initPush = next => {
|
|
146
|
+
const first = config === null;
|
|
147
|
+
config = next;
|
|
148
|
+
if (!first) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const mods = resolveModules();
|
|
152
|
+
if (!mods) {
|
|
153
|
+
setState({
|
|
154
|
+
status: 'unavailable'
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const m = mods.notifications;
|
|
159
|
+
m.setNotificationHandler({
|
|
160
|
+
handleNotification: async () => {
|
|
161
|
+
const show = config?.showInForeground?.() ?? true;
|
|
162
|
+
return {
|
|
163
|
+
shouldShowBanner: show,
|
|
164
|
+
shouldShowList: show,
|
|
165
|
+
shouldPlaySound: show,
|
|
166
|
+
shouldSetBadge: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
if (Platform.OS === 'android' && next.androidChannels?.length) {
|
|
171
|
+
channelsReady = createChannels(m, next.androidChannels);
|
|
172
|
+
}
|
|
173
|
+
m.addNotificationResponseReceivedListener(handleResponse);
|
|
174
|
+
void m.getLastNotificationResponseAsync().then(response => {
|
|
175
|
+
handleResponse(response);
|
|
176
|
+
return m.clearLastNotificationResponseAsync?.();
|
|
177
|
+
}).catch(error => Logger.error('[push] reading the launch notification failed', error));
|
|
178
|
+
void m.getPermissionsAsync().then(response => setState({
|
|
179
|
+
status: toPermissionStatus(response)
|
|
180
|
+
})).catch(error => Logger.error('[push] permission check failed', error));
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Asks for permission, reads the native device token and registers it through
|
|
185
|
+
* `config.register`, then re-registers whenever the OS rotates the token.
|
|
186
|
+
*/
|
|
187
|
+
export const enablePush = async () => {
|
|
188
|
+
requireConfig();
|
|
189
|
+
const mods = resolveModules();
|
|
190
|
+
if (!mods) {
|
|
191
|
+
setState({
|
|
192
|
+
status: 'unavailable'
|
|
193
|
+
});
|
|
194
|
+
return 'unavailable';
|
|
195
|
+
}
|
|
196
|
+
const m = mods.notifications;
|
|
197
|
+
|
|
198
|
+
// Android 13 shows the permission prompt only once a channel exists.
|
|
199
|
+
await channelsReady;
|
|
200
|
+
let permission = await m.getPermissionsAsync();
|
|
201
|
+
if (!permission.granted) {
|
|
202
|
+
permission = await m.requestPermissionsAsync();
|
|
203
|
+
}
|
|
204
|
+
if (!permission.granted) {
|
|
205
|
+
setState({
|
|
206
|
+
status: 'denied'
|
|
207
|
+
});
|
|
208
|
+
return 'denied';
|
|
209
|
+
}
|
|
210
|
+
const device = await toDevice(await m.getDevicePushTokenAsync(), mods.applicationId);
|
|
211
|
+
if (!device) {
|
|
212
|
+
setState({
|
|
213
|
+
status: 'unavailable'
|
|
214
|
+
});
|
|
215
|
+
return 'unavailable';
|
|
216
|
+
}
|
|
217
|
+
await requireConfig().register(device);
|
|
218
|
+
tokenSubscription?.remove();
|
|
219
|
+
tokenSubscription = m.addPushTokenListener(token => {
|
|
220
|
+
void toDevice(token, mods.applicationId).then(next => next ? requireConfig().register(next) : undefined).catch(error => Logger.error('[push] re-registering a new token failed', error));
|
|
221
|
+
});
|
|
222
|
+
setState({
|
|
223
|
+
status: 'granted',
|
|
224
|
+
enabled: true
|
|
225
|
+
});
|
|
226
|
+
return 'granted';
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Unregisters the device through `config.unregister` and stops following token
|
|
231
|
+
* changes. Taps on notifications already delivered still reach `onOpen`.
|
|
232
|
+
*/
|
|
233
|
+
export const disablePush = async () => {
|
|
234
|
+
const current = requireConfig();
|
|
235
|
+
await current.unregister(await current.getInstallationId());
|
|
236
|
+
tokenSubscription?.remove();
|
|
237
|
+
tokenSubscription = null;
|
|
238
|
+
setState({
|
|
239
|
+
enabled: false
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
244
|
+
export const setBadgeCount = async count => {
|
|
245
|
+
const mods = resolveModules();
|
|
246
|
+
if (!mods) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await mods.notifications.setBadgeCountAsync(count);
|
|
250
|
+
};
|
|
251
|
+
const subscribe = notify => {
|
|
252
|
+
subscribers.add(notify);
|
|
253
|
+
return () => {
|
|
254
|
+
subscribers.delete(notify);
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
const getSnapshot = () => state;
|
|
258
|
+
const enable = async () => {
|
|
259
|
+
await enablePush();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** Push status for a settings switch, re-rendering when it changes. */
|
|
263
|
+
export const usePush = () => {
|
|
264
|
+
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
265
|
+
return {
|
|
266
|
+
...snapshot,
|
|
267
|
+
enable,
|
|
268
|
+
disable: disablePush
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=push.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { PushAndroidChannel, PushConfig, PushDevice, PushModules, PushNotificationsLike, PushPlatform, PushState, PushStatus } from './push';
|
|
2
|
+
export { disablePush, enablePush, initPush, setBadgeCount, setPushImplementation, usePush } from './push';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type PushPlatform = 'Fcm' | 'Apns';
|
|
2
|
+
export interface PushDevice {
|
|
3
|
+
installationId: string;
|
|
4
|
+
platform: PushPlatform;
|
|
5
|
+
token: string;
|
|
6
|
+
/** The app's bundle id, the APNs topic on iOS. */
|
|
7
|
+
appId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PushAndroidChannel {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
importance?: 'default' | 'high';
|
|
13
|
+
}
|
|
14
|
+
export interface PushConfig {
|
|
15
|
+
getInstallationId: () => Promise<string> | string;
|
|
16
|
+
/** Sends the device to the app's own API. */
|
|
17
|
+
register: (device: PushDevice) => Promise<void>;
|
|
18
|
+
/** Removes the device from the app's own API. */
|
|
19
|
+
unregister: (installationId: string) => Promise<void>;
|
|
20
|
+
/** A notification was tapped, including the tap that launched the app. */
|
|
21
|
+
onOpen: (data: Record<string, string>) => void;
|
|
22
|
+
/** Only for channels the app does not create natively. */
|
|
23
|
+
androidChannels?: PushAndroidChannel[];
|
|
24
|
+
/** Whether a notification arriving while the app is open is shown. Defaults to true. */
|
|
25
|
+
showInForeground?: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export type PushStatus = 'unknown' | 'granted' | 'denied' | 'unavailable';
|
|
28
|
+
export interface PushState {
|
|
29
|
+
status: PushStatus;
|
|
30
|
+
/** True once this process has registered the device, false after `disablePush`. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface PermissionResponseLike {
|
|
34
|
+
status: 'granted' | 'denied' | 'undetermined';
|
|
35
|
+
granted: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface SubscriptionLike {
|
|
38
|
+
remove: () => void;
|
|
39
|
+
}
|
|
40
|
+
interface NotificationResponseLike {
|
|
41
|
+
actionIdentifier: string;
|
|
42
|
+
notification: {
|
|
43
|
+
request: {
|
|
44
|
+
identifier: string;
|
|
45
|
+
content: {
|
|
46
|
+
data?: Record<string, unknown> | null;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface DevicePushTokenLike {
|
|
52
|
+
type: string;
|
|
53
|
+
data: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** The slice of expo-notifications this module calls, so the optional peer stays untyped at build. */
|
|
56
|
+
export interface PushNotificationsLike {
|
|
57
|
+
getPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
58
|
+
requestPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
59
|
+
getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
|
|
60
|
+
addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
|
|
61
|
+
addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
|
|
62
|
+
getLastNotificationResponseAsync: () => Promise<NotificationResponseLike | null>;
|
|
63
|
+
clearLastNotificationResponseAsync?: () => Promise<void>;
|
|
64
|
+
setNotificationHandler: (handler: {
|
|
65
|
+
handleNotification: () => Promise<{
|
|
66
|
+
shouldShowBanner: boolean;
|
|
67
|
+
shouldShowList: boolean;
|
|
68
|
+
shouldPlaySound: boolean;
|
|
69
|
+
shouldSetBadge: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
} | null) => void;
|
|
72
|
+
setNotificationChannelAsync: (id: string, channel: {
|
|
73
|
+
name: string;
|
|
74
|
+
importance: number;
|
|
75
|
+
}) => Promise<unknown>;
|
|
76
|
+
setBadgeCountAsync: (count: number) => Promise<boolean>;
|
|
77
|
+
AndroidImportance: {
|
|
78
|
+
DEFAULT: number;
|
|
79
|
+
HIGH: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface PushModules {
|
|
83
|
+
notifications: PushNotificationsLike;
|
|
84
|
+
/** The bundle id, from expo-application. */
|
|
85
|
+
applicationId: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Replace the native modules. For tests and the showcase, to reach paths a
|
|
89
|
+
* real device never takes; an injected backend is taken on trust, Expo Go
|
|
90
|
+
* included. Pass `null` to restore expo-notifications.
|
|
91
|
+
*/
|
|
92
|
+
export declare const setPushImplementation: (impl: PushModules | null) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Wires push at startup: the foreground handler, Android channels, and tap
|
|
95
|
+
* handling, including the tap that launched the app. Calling it again only
|
|
96
|
+
* replaces the config.
|
|
97
|
+
*/
|
|
98
|
+
export declare const initPush: (next: PushConfig) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Asks for permission, reads the native device token and registers it through
|
|
101
|
+
* `config.register`, then re-registers whenever the OS rotates the token.
|
|
102
|
+
*/
|
|
103
|
+
export declare const enablePush: () => Promise<"granted" | "denied" | "unavailable">;
|
|
104
|
+
/**
|
|
105
|
+
* Unregisters the device through `config.unregister` and stops following token
|
|
106
|
+
* changes. Taps on notifications already delivered still reach `onOpen`.
|
|
107
|
+
*/
|
|
108
|
+
export declare const disablePush: () => Promise<void>;
|
|
109
|
+
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
110
|
+
export declare const setBadgeCount: (count: number) => Promise<void>;
|
|
111
|
+
/** Push status for a settings switch, re-rendering when it changes. */
|
|
112
|
+
export declare const usePush: () => PushState & {
|
|
113
|
+
enable: () => Promise<void>;
|
|
114
|
+
disable: () => Promise<void>;
|
|
115
|
+
};
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=push.d.ts.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { PushAndroidChannel, PushConfig, PushDevice, PushModules, PushNotificationsLike, PushPlatform, PushState, PushStatus } from './push.js';
|
|
2
|
+
export { disablePush, enablePush, initPush, setBadgeCount, setPushImplementation, usePush } from './push.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export type PushPlatform = 'Fcm' | 'Apns';
|
|
2
|
+
export interface PushDevice {
|
|
3
|
+
installationId: string;
|
|
4
|
+
platform: PushPlatform;
|
|
5
|
+
token: string;
|
|
6
|
+
/** The app's bundle id, the APNs topic on iOS. */
|
|
7
|
+
appId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PushAndroidChannel {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
importance?: 'default' | 'high';
|
|
13
|
+
}
|
|
14
|
+
export interface PushConfig {
|
|
15
|
+
getInstallationId: () => Promise<string> | string;
|
|
16
|
+
/** Sends the device to the app's own API. */
|
|
17
|
+
register: (device: PushDevice) => Promise<void>;
|
|
18
|
+
/** Removes the device from the app's own API. */
|
|
19
|
+
unregister: (installationId: string) => Promise<void>;
|
|
20
|
+
/** A notification was tapped, including the tap that launched the app. */
|
|
21
|
+
onOpen: (data: Record<string, string>) => void;
|
|
22
|
+
/** Only for channels the app does not create natively. */
|
|
23
|
+
androidChannels?: PushAndroidChannel[];
|
|
24
|
+
/** Whether a notification arriving while the app is open is shown. Defaults to true. */
|
|
25
|
+
showInForeground?: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export type PushStatus = 'unknown' | 'granted' | 'denied' | 'unavailable';
|
|
28
|
+
export interface PushState {
|
|
29
|
+
status: PushStatus;
|
|
30
|
+
/** True once this process has registered the device, false after `disablePush`. */
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface PermissionResponseLike {
|
|
34
|
+
status: 'granted' | 'denied' | 'undetermined';
|
|
35
|
+
granted: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface SubscriptionLike {
|
|
38
|
+
remove: () => void;
|
|
39
|
+
}
|
|
40
|
+
interface NotificationResponseLike {
|
|
41
|
+
actionIdentifier: string;
|
|
42
|
+
notification: {
|
|
43
|
+
request: {
|
|
44
|
+
identifier: string;
|
|
45
|
+
content: {
|
|
46
|
+
data?: Record<string, unknown> | null;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface DevicePushTokenLike {
|
|
52
|
+
type: string;
|
|
53
|
+
data: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** The slice of expo-notifications this module calls, so the optional peer stays untyped at build. */
|
|
56
|
+
export interface PushNotificationsLike {
|
|
57
|
+
getPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
58
|
+
requestPermissionsAsync: () => Promise<PermissionResponseLike>;
|
|
59
|
+
getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
|
|
60
|
+
addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
|
|
61
|
+
addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
|
|
62
|
+
getLastNotificationResponseAsync: () => Promise<NotificationResponseLike | null>;
|
|
63
|
+
clearLastNotificationResponseAsync?: () => Promise<void>;
|
|
64
|
+
setNotificationHandler: (handler: {
|
|
65
|
+
handleNotification: () => Promise<{
|
|
66
|
+
shouldShowBanner: boolean;
|
|
67
|
+
shouldShowList: boolean;
|
|
68
|
+
shouldPlaySound: boolean;
|
|
69
|
+
shouldSetBadge: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
} | null) => void;
|
|
72
|
+
setNotificationChannelAsync: (id: string, channel: {
|
|
73
|
+
name: string;
|
|
74
|
+
importance: number;
|
|
75
|
+
}) => Promise<unknown>;
|
|
76
|
+
setBadgeCountAsync: (count: number) => Promise<boolean>;
|
|
77
|
+
AndroidImportance: {
|
|
78
|
+
DEFAULT: number;
|
|
79
|
+
HIGH: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface PushModules {
|
|
83
|
+
notifications: PushNotificationsLike;
|
|
84
|
+
/** The bundle id, from expo-application. */
|
|
85
|
+
applicationId: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Replace the native modules. For tests and the showcase, to reach paths a
|
|
89
|
+
* real device never takes; an injected backend is taken on trust, Expo Go
|
|
90
|
+
* included. Pass `null` to restore expo-notifications.
|
|
91
|
+
*/
|
|
92
|
+
export declare const setPushImplementation: (impl: PushModules | null) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Wires push at startup: the foreground handler, Android channels, and tap
|
|
95
|
+
* handling, including the tap that launched the app. Calling it again only
|
|
96
|
+
* replaces the config.
|
|
97
|
+
*/
|
|
98
|
+
export declare const initPush: (next: PushConfig) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Asks for permission, reads the native device token and registers it through
|
|
101
|
+
* `config.register`, then re-registers whenever the OS rotates the token.
|
|
102
|
+
*/
|
|
103
|
+
export declare const enablePush: () => Promise<"granted" | "denied" | "unavailable">;
|
|
104
|
+
/**
|
|
105
|
+
* Unregisters the device through `config.unregister` and stops following token
|
|
106
|
+
* changes. Taps on notifications already delivered still reach `onOpen`.
|
|
107
|
+
*/
|
|
108
|
+
export declare const disablePush: () => Promise<void>;
|
|
109
|
+
/** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
|
|
110
|
+
export declare const setBadgeCount: (count: number) => Promise<void>;
|
|
111
|
+
/** Push status for a settings switch, re-rendering when it changes. */
|
|
112
|
+
export declare const usePush: () => PushState & {
|
|
113
|
+
enable: () => Promise<void>;
|
|
114
|
+
disable: () => Promise<void>;
|
|
115
|
+
};
|
|
116
|
+
export {};
|
|
117
|
+
//# sourceMappingURL=push.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority/mobile-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage and version checks. No UI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -161,6 +161,16 @@
|
|
|
161
161
|
"default": "./lib/commonjs/share/index.js"
|
|
162
162
|
}
|
|
163
163
|
},
|
|
164
|
+
"./push": {
|
|
165
|
+
"import": {
|
|
166
|
+
"types": "./lib/typescript/module/push/index.d.ts",
|
|
167
|
+
"default": "./lib/module/push/index.js"
|
|
168
|
+
},
|
|
169
|
+
"require": {
|
|
170
|
+
"types": "./lib/typescript/commonjs/push/index.d.ts",
|
|
171
|
+
"default": "./lib/commonjs/push/index.js"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
164
174
|
"./smsRetriever": {
|
|
165
175
|
"import": {
|
|
166
176
|
"types": "./lib/typescript/module/smsRetriever/index.d.ts",
|