@bigbinary/neeto-commons-rn 2.2.1 → 2.2.5
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/lib/typescript/components/PushPermissionGate.d.ts +20 -0
- package/lib/typescript/components/PushPermissionGate.d.ts.map +1 -0
- package/lib/typescript/components/SocialButton.d.ts.map +1 -1
- package/lib/typescript/components/index.d.ts +1 -0
- package/lib/typescript/components/index.d.ts.map +1 -1
- package/lib/typescript/config/bugsnag.d.ts.map +1 -1
- package/lib/typescript/constants/index.d.ts +11 -0
- package/lib/typescript/constants/index.d.ts.map +1 -1
- package/lib/typescript/helpers/notifications/index.d.ts +1 -0
- package/lib/typescript/helpers/notifications/index.d.ts.map +1 -1
- package/lib/typescript/helpers/notifications/permission.d.ts +40 -0
- package/lib/typescript/helpers/notifications/permission.d.ts.map +1 -0
- package/lib/typescript/helpers/notifications/registerBackgroundHandlers.d.ts.map +1 -1
- package/lib/typescript/helpers/notifications/usePushNotification.d.ts.map +1 -1
- package/lib/typescript/hooks/index.d.ts +2 -0
- package/lib/typescript/hooks/index.d.ts.map +1 -1
- package/lib/typescript/hooks/useNotificationPermissionStatus.d.ts +16 -0
- package/lib/typescript/hooks/useNotificationPermissionStatus.d.ts.map +1 -0
- package/lib/typescript/hooks/usePushNotificationPermissions.d.ts +8 -0
- package/lib/typescript/hooks/usePushNotificationPermissions.d.ts.map +1 -1
- package/lib/typescript/hooks/useRequestNotificationPermission.d.ts +13 -0
- package/lib/typescript/hooks/useRequestNotificationPermission.d.ts.map +1 -0
- package/lib/typescript/providers/NeetoCommonsRNProvider.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/AuthOptions.d.ts.map +1 -1
- package/lib/typescript/screens/authentication/OTPVerificationScreen.d.ts.map +1 -1
- package/package.json +32 -4
- package/src/components/PushPermissionGate.ts +25 -0
- package/src/components/SocialButton.tsx +2 -1
- package/src/components/index.ts +1 -0
- package/src/config/bugsnag.ts +12 -5
- package/src/constants/index.ts +12 -0
- package/src/helpers/notifications/index.ts +4 -0
- package/src/helpers/notifications/permission.ts +56 -0
- package/src/helpers/notifications/registerBackgroundHandlers.ts +17 -6
- package/src/helpers/notifications/usePushNotification.ts +17 -10
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useNotificationPermissionStatus.ts +50 -0
- package/src/hooks/usePushNotificationPermissions.ts +20 -9
- package/src/hooks/useRequestNotificationPermission.ts +55 -0
- package/src/providers/NeetoCommonsRNProvider.tsx +11 -8
- package/src/screens/authentication/AuthOptions.tsx +5 -0
- package/src/screens/authentication/OTPVerificationScreen.tsx +7 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raises the notification permission prompt, once, and renders nothing.
|
|
3
|
+
*
|
|
4
|
+
* Mount it inside the signed-in branch of the navigator:
|
|
5
|
+
*
|
|
6
|
+
* {isLoggedIn && <PushPermissionGate />}
|
|
7
|
+
*
|
|
8
|
+
* A component rather than a hook call because the request happens as soon as
|
|
9
|
+
* it runs and hooks cannot be called conditionally -- mounting is what gates
|
|
10
|
+
* it on auth, and what keeps the prompt off the login screen. Unmounting on
|
|
11
|
+
* sign-out means the next account on a shared device is asked on its own
|
|
12
|
+
* first login rather than inheriting the previous session's state.
|
|
13
|
+
*
|
|
14
|
+
* This exists because every app that put the request on its Profile screen
|
|
15
|
+
* shipped the same bug: Profile is a lazily mounted tab, so a user who never
|
|
16
|
+
* opened it was never asked, while the app went on registering a device token
|
|
17
|
+
* for them. See neetozone/neeto-chat-rn#788.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PushPermissionGate: () => null;
|
|
20
|
+
//# sourceMappingURL=PushPermissionGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PushPermissionGate.d.ts","sourceRoot":"","sources":["../../../src/components/PushPermissionGate.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,YAI9B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SocialButton.d.ts","sourceRoot":"","sources":["../../../src/components/SocialButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"SocialButton.d.ts","sourceRoot":"","sources":["../../../src/components/SocialButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAkB1E,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAKD,eAAO,MAAM,YAAY,GAAI,mDAM1B,iBAAiB,sBAyBnB,CAAC"}
|
|
@@ -11,4 +11,5 @@ export { EmptyView } from "./EmptyView";
|
|
|
11
11
|
export { TextWithUrl } from "./TextWithUrl";
|
|
12
12
|
export { OfflineScreen } from "./OfflineScreen";
|
|
13
13
|
export { WorkspaceSwitcherPane } from "./WorkspaceSwitcherPane";
|
|
14
|
+
export { PushPermissionGate } from "./PushPermissionGate";
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bugsnag.d.ts","sourceRoot":"","sources":["../../../src/config/bugsnag.ts"],"names":[],"mappings":"AACA,OAAgB,EACd,KAAK,KAAK,EACV,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAgD/B,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,KAMlD,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,KAAK,eAAe,EACpB,QAAQ,MAAM,EAGd,eAAe,MAAM,EAGrB,WAAU,KAAK,CAAC,UAAU,CAAa,SAkBxC,CAAC;AAEF,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"bugsnag.d.ts","sourceRoot":"","sources":["../../../src/config/bugsnag.ts"],"names":[],"mappings":"AACA,OAAgB,EACd,KAAK,KAAK,EACV,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AAgD/B,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,KAMlD,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,KAAK,eAAe,EACpB,QAAQ,MAAM,EAGd,eAAe,MAAM,EAGrB,WAAU,KAAK,CAAC,UAAU,CAAa,SAkBxC,CAAC;AAEF,eAAO,MAAM,gBAAgB,YAuD5B,CAAC"}
|
|
@@ -3,6 +3,16 @@ export declare const STORAGE_KEYS: {
|
|
|
3
3
|
readonly DEVICE_TOKEN: "devicetoken";
|
|
4
4
|
readonly IS_VISITED: "isVisited";
|
|
5
5
|
readonly NOTIFICATION_PERMISSION_REQUESTED: "notificationPermissionRequested";
|
|
6
|
+
/**
|
|
7
|
+
* Read-only fallback. Before this key existed here, every app kept its own
|
|
8
|
+
* "have we asked yet?" flag under this exact string -- neeto-desk,
|
|
9
|
+
* neeto-cal, neeto-form and neeto-invoice all wrote it verbatim from their
|
|
10
|
+
* own constants. An app adopting the hooks here would otherwise start with
|
|
11
|
+
* a null flag and re-prompt everyone who had already declined, so the
|
|
12
|
+
* request path reads this when its own key is unset. Never written; drop it
|
|
13
|
+
* once installs have rolled over.
|
|
14
|
+
*/
|
|
15
|
+
readonly LEGACY_APP_NOTIFICATION_PROMPT_DONE: "notification_first_prompt_done";
|
|
6
16
|
};
|
|
7
17
|
export declare const DEFAULT_PAGE_SIZE = 30;
|
|
8
18
|
/**
|
|
@@ -13,6 +23,7 @@ export declare const DEFAULT_PAGE_SIZE = 30;
|
|
|
13
23
|
export declare const APP_VERSION_ERROR_REPORT_INTERVAL: number;
|
|
14
24
|
/** One Bugsnag group for every app_versions failure, whatever the status. */
|
|
15
25
|
export declare const APP_VERSION_ERROR_GROUPING_HASH = "app-version-check-failed";
|
|
26
|
+
export declare const AUTH_BUTTON_HEIGHT = 48;
|
|
16
27
|
/** Design baseline: a ~5" phone, the width every scaled size was drawn against. */
|
|
17
28
|
export declare const GUIDELINE_BASE_WIDTH = 350;
|
|
18
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/constants/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,YAAY;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/constants/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,eAAO,MAAM,YAAY;;;;IAIvB;;;;;;;;OAQG;;CAEK,CAAC;AAEX,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,QAAiB,CAAC;AAEhE,6EAA6E;AAC7E,eAAO,MAAM,+BAA+B,6BAA6B,CAAC;AAE1E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,mFAAmF;AACnF,eAAO,MAAM,oBAAoB,MAAM,CAAC"}
|
|
@@ -2,4 +2,5 @@ export { usePushNotification } from "./usePushNotification";
|
|
|
2
2
|
export { registerBackgroundHandlers } from "./registerBackgroundHandlers";
|
|
3
3
|
export { NOTIFICATION_CHANNELS, NOTIFICATION_CHANNEL_IDS } from "./constants";
|
|
4
4
|
export { getInitialFirebaseNotification } from "./firebaseMessaging";
|
|
5
|
+
export { hasBeenPromptedForNotifications, needsNotificationPermissionPrompt, } from "./permission";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC9E,OAAO,EAAE,8BAA8B,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EACL,+BAA+B,EAC/B,iCAAiC,GAClC,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
/**
|
|
3
|
+
* Whether this device has a notification permission worth asking for.
|
|
4
|
+
*
|
|
5
|
+
* Android only gained a runtime notification permission in 13 (API 33);
|
|
6
|
+
* below that the manifest entry is the whole story and there is nothing to
|
|
7
|
+
* prompt for. Every iOS version needs asking.
|
|
8
|
+
*
|
|
9
|
+
* Consuming apps used to hand-roll this as
|
|
10
|
+
* `Platform.OS !== "android" || Platform.Version < 33`, which returns early
|
|
11
|
+
* on iOS as well and so never asked there at all. neeto-desk shipped that
|
|
12
|
+
* for months; neeto-form and neeto-invoice still carry the same line. Owning
|
|
13
|
+
* it here is the point of this module.
|
|
14
|
+
*
|
|
15
|
+
* Note that `Platform.Version` is a number on Android and a string on iOS,
|
|
16
|
+
* so the version comparison is only meaningful once the OS is known to be
|
|
17
|
+
* Android. Coercing keeps a string version from silently comparing as NaN.
|
|
18
|
+
*
|
|
19
|
+
* Parameters are injectable so the platform matrix is testable without
|
|
20
|
+
* mutating the Platform module.
|
|
21
|
+
*/
|
|
22
|
+
export declare const needsNotificationPermissionPrompt: (os?: typeof Platform.OS, version?: typeof Platform.Version) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Has this install already been through the prompt?
|
|
25
|
+
*
|
|
26
|
+
* Falls back to the app-local flag every consuming app used to keep under
|
|
27
|
+
* "notification_first_prompt_done". Without that read, an app adopting these
|
|
28
|
+
* hooks starts with a null flag, decides nobody has been asked, and
|
|
29
|
+
* re-prompts everyone whose status is still DENIED -- on Android 13+ that is
|
|
30
|
+
* every user who declined exactly once, since Android allows a second ask.
|
|
31
|
+
* iOS is unaffected either way: a prior denial reads BLOCKED, not DENIED.
|
|
32
|
+
*
|
|
33
|
+
* A fallback read rather than a migration, on purpose. A migration would have
|
|
34
|
+
* to finish before the requesting hook mounts -- it prompts on mount -- which
|
|
35
|
+
* means gating the gate on an async seed. Reading both keys has no ordering
|
|
36
|
+
* to get wrong, is idempotent, and covers every app at once because they all
|
|
37
|
+
* wrote the identical string.
|
|
38
|
+
*/
|
|
39
|
+
export declare const hasBeenPromptedForNotifications: () => Promise<boolean>;
|
|
40
|
+
//# sourceMappingURL=permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKxC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iCAAiC,GAC5C,KAAI,OAAO,QAAQ,CAAC,EAAgB,EACpC,UAAS,OAAO,QAAQ,CAAC,OAA0B,YACP,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,wBAS3C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registerBackgroundHandlers.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/registerBackgroundHandlers.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAOxD,eAAO,MAAM,0BAA0B,GACrC,sBAAsB,wBAAwB,
|
|
1
|
+
{"version":3,"file":"registerBackgroundHandlers.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/registerBackgroundHandlers.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAOxD,eAAO,MAAM,0BAA0B,GACrC,sBAAsB,wBAAwB,SA0D/C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePushNotification.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/usePushNotification.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,wBAAwB,EAE9B,MAAM,SAAS,CAAC;AAWjB,UAAU,uBAAuB;IAC/B,WAAW,EAAE,wBAAwB,CAAC;CACvC;AAED,eAAO,MAAM,mBAAmB,GAC9B,YAAY,OAAO,EACnB,kBAAiB,uBAAmD,
|
|
1
|
+
{"version":3,"file":"usePushNotification.d.ts","sourceRoot":"","sources":["../../../../src/helpers/notifications/usePushNotification.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,wBAAwB,EAE9B,MAAM,SAAS,CAAC;AAWjB,UAAU,uBAAuB;IAC/B,WAAW,EAAE,wBAAwB,CAAC;CACvC;AAED,eAAO,MAAM,mBAAmB,GAC9B,YAAY,OAAO,EACnB,kBAAiB,uBAAmD,YAwLrE,CAAC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { useLocalization } from "../contexts/localization";
|
|
2
2
|
export { usePushNotificationPermissions } from "./usePushNotificationPermissions";
|
|
3
|
+
export { useNotificationPermissionStatus } from "./useNotificationPermissionStatus";
|
|
4
|
+
export { useRequestNotificationPermission } from "./useRequestNotificationPermission";
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,8BAA8B,EAAE,MAAM,kCAAkC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,8BAA8B,EAAE,MAAM,kCAAkC,CAAC;AAClF,OAAO,EAAE,+BAA+B,EAAE,MAAM,mCAAmC,CAAC;AACpF,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only view of the notification permission.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately never requests. A screen that merely displays whether
|
|
5
|
+
* notifications are on -- every app's Profile -- must not be the thing that
|
|
6
|
+
* asks, or users who never open it are never asked at all. That is exactly
|
|
7
|
+
* what usePushNotificationPermissions caused by combining the two: it was
|
|
8
|
+
* called from Profile because Profile is where the status is shown, which
|
|
9
|
+
* made a settings screen the app's only requester. Use PushPermissionGate
|
|
10
|
+
* for the asking.
|
|
11
|
+
*
|
|
12
|
+
* Re-checks on foreground, because the only way back from a denial is the
|
|
13
|
+
* system Settings app, which means leaving the app and returning.
|
|
14
|
+
*/
|
|
15
|
+
export declare const useNotificationPermissionStatus: () => boolean;
|
|
16
|
+
//# sourceMappingURL=useNotificationPermissionStatus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNotificationPermissionStatus.d.ts","sourceRoot":"","sources":["../../../src/hooks/useNotificationPermissionStatus.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,+BAA+B,eA4B3C,CAAC"}
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Checks and requests in one call, which is why every app that
|
|
3
|
+
* wanted to *display* the status ended up making its Profile screen the only
|
|
4
|
+
* thing that ever *asked* -- see neetozone/neeto-chat-rn#788. Prefer
|
|
5
|
+
* `useNotificationPermissionStatus()` to read and `<PushPermissionGate />`
|
|
6
|
+
* (or `useRequestNotificationPermission()`) to ask. Kept working for
|
|
7
|
+
* consumers that have not moved yet.
|
|
8
|
+
*/
|
|
1
9
|
export declare const usePushNotificationPermissions: () => boolean[];
|
|
2
10
|
//# sourceMappingURL=usePushNotificationPermissions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePushNotificationPermissions.d.ts","sourceRoot":"","sources":["../../../src/hooks/usePushNotificationPermissions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"usePushNotificationPermissions.d.ts","sourceRoot":"","sources":["../../../src/hooks/usePushNotificationPermissions.ts"],"names":[],"mappings":"AAkBA;;;;;;;GAOG;AACH,eAAO,MAAM,8BAA8B,iBA+F1C,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asks for notification permission once per install, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Prefer mounting PushPermissionGate inside the signed-in branch of the
|
|
5
|
+
* navigator; this hook is what it wraps, exported for apps that need to
|
|
6
|
+
* trigger the request from somewhere other than a mount.
|
|
7
|
+
*
|
|
8
|
+
* Only asks when the status is DENIED. GRANTED needs nothing and BLOCKED
|
|
9
|
+
* cannot be reopened from inside the app -- sending a blocked user to
|
|
10
|
+
* Settings is denied-state recovery, which belongs to the UI, not here.
|
|
11
|
+
*/
|
|
12
|
+
export declare const useRequestNotificationPermission: (enabled?: boolean) => void;
|
|
13
|
+
//# sourceMappingURL=useRequestNotificationPermission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRequestNotificationPermission.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRequestNotificationPermission.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAAI,iBAAc,SA2B9D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NeetoCommonsRNProvider.d.ts","sourceRoot":"","sources":["../../../src/providers/NeetoCommonsRNProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAIZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AAIrC,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAC;AAyBlC,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,sBAAsB,CACxD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CACnC,GAAG,IAAI,CAAC,CAAC;IACV,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,eAAO,MAAM,sBAAsB,GAAI,2GAQpC,2BAA2B,
|
|
1
|
+
{"version":3,"file":"NeetoCommonsRNProvider.d.ts","sourceRoot":"","sources":["../../../src/providers/NeetoCommonsRNProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAIZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;AAIrC,OAAO,EAEL,KAAK,WAAW,EACjB,MAAM,0BAA0B,CAAC;AAyBlC,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,sBAAsB,CACxD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CACnC,GAAG,IAAI,CAAC,CAAC;IACV,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,eAAO,MAAM,sBAAsB,GAAI,2GAQpC,2BAA2B,sBAmC7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthOptions.d.ts","sourceRoot":"","sources":["../../../../src/screens/authentication/AuthOptions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthOptions.d.ts","sourceRoot":"","sources":["../../../../src/screens/authentication/AuthOptions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAkC1B,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,WAAW,GAAI,kCAGzB,gBAAgB,sBAkMlB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OTPVerificationScreen.d.ts","sourceRoot":"","sources":["../../../../src/screens/authentication/OTPVerificationScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"OTPVerificationScreen.d.ts","sourceRoot":"","sources":["../../../../src/screens/authentication/OTPVerificationScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAmBxE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAKjE,eAAO,MAAM,qBAAqB,GAAI,oCAGnC,kBAAkB,CAAC,iBAAiB,CAAC,sBA6JvC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bigbinary/neeto-commons-rn",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"react-native": "./src/index.ts",
|
|
@@ -121,6 +121,11 @@
|
|
|
121
121
|
"react-native": "./src/components/OrganizationItem.tsx",
|
|
122
122
|
"default": "./src/components/OrganizationItem.tsx"
|
|
123
123
|
},
|
|
124
|
+
"./components/PushPermissionGate": {
|
|
125
|
+
"types": "./lib/typescript/components/PushPermissionGate.d.ts",
|
|
126
|
+
"react-native": "./src/components/PushPermissionGate.ts",
|
|
127
|
+
"default": "./src/components/PushPermissionGate.ts"
|
|
128
|
+
},
|
|
124
129
|
"./components/SocialButton": {
|
|
125
130
|
"types": "./lib/typescript/components/SocialButton.d.ts",
|
|
126
131
|
"react-native": "./src/components/SocialButton.tsx",
|
|
@@ -181,6 +186,11 @@
|
|
|
181
186
|
"react-native": "./src/config/axios/paramsSerializer.ts",
|
|
182
187
|
"default": "./src/config/axios/paramsSerializer.ts"
|
|
183
188
|
},
|
|
189
|
+
"./config/axios/redact": {
|
|
190
|
+
"types": "./lib/typescript/config/axios/redact.d.ts",
|
|
191
|
+
"react-native": "./src/config/axios/redact.ts",
|
|
192
|
+
"default": "./src/config/axios/redact.ts"
|
|
193
|
+
},
|
|
184
194
|
"./config/axios/types": {
|
|
185
195
|
"types": "./lib/typescript/config/axios/types.d.ts",
|
|
186
196
|
"react-native": "./src/config/axios/types.ts",
|
|
@@ -261,6 +271,11 @@
|
|
|
261
271
|
"react-native": "./src/helpers/notifications/index.ts",
|
|
262
272
|
"default": "./src/helpers/notifications/index.ts"
|
|
263
273
|
},
|
|
274
|
+
"./helpers/notifications/permission": {
|
|
275
|
+
"types": "./lib/typescript/helpers/notifications/permission.d.ts",
|
|
276
|
+
"react-native": "./src/helpers/notifications/permission.ts",
|
|
277
|
+
"default": "./src/helpers/notifications/permission.ts"
|
|
278
|
+
},
|
|
264
279
|
"./helpers/notifications/registerBackgroundHandlers": {
|
|
265
280
|
"types": "./lib/typescript/helpers/notifications/registerBackgroundHandlers.d.ts",
|
|
266
281
|
"react-native": "./src/helpers/notifications/registerBackgroundHandlers.ts",
|
|
@@ -331,16 +346,31 @@
|
|
|
331
346
|
"react-native": "./src/hooks/useAppState.ts",
|
|
332
347
|
"default": "./src/hooks/useAppState.ts"
|
|
333
348
|
},
|
|
349
|
+
"./hooks/useNotificationPermissionStatus": {
|
|
350
|
+
"types": "./lib/typescript/hooks/useNotificationPermissionStatus.d.ts",
|
|
351
|
+
"react-native": "./src/hooks/useNotificationPermissionStatus.ts",
|
|
352
|
+
"default": "./src/hooks/useNotificationPermissionStatus.ts"
|
|
353
|
+
},
|
|
334
354
|
"./hooks/usePushNotificationPermissions": {
|
|
335
355
|
"types": "./lib/typescript/hooks/usePushNotificationPermissions.d.ts",
|
|
336
356
|
"react-native": "./src/hooks/usePushNotificationPermissions.ts",
|
|
337
357
|
"default": "./src/hooks/usePushNotificationPermissions.ts"
|
|
338
358
|
},
|
|
359
|
+
"./hooks/useRequestNotificationPermission": {
|
|
360
|
+
"types": "./lib/typescript/hooks/useRequestNotificationPermission.d.ts",
|
|
361
|
+
"react-native": "./src/hooks/useRequestNotificationPermission.ts",
|
|
362
|
+
"default": "./src/hooks/useRequestNotificationPermission.ts"
|
|
363
|
+
},
|
|
339
364
|
".": {
|
|
340
365
|
"types": "./lib/typescript/index.d.ts",
|
|
341
366
|
"react-native": "./src/index.ts",
|
|
342
367
|
"default": "./src/index.ts"
|
|
343
368
|
},
|
|
369
|
+
"./initializers/constants": {
|
|
370
|
+
"types": "./lib/typescript/initializers/constants.d.ts",
|
|
371
|
+
"react-native": "./src/initializers/constants.ts",
|
|
372
|
+
"default": "./src/initializers/constants.ts"
|
|
373
|
+
},
|
|
344
374
|
"./initializers": {
|
|
345
375
|
"types": "./lib/typescript/initializers/index.d.ts",
|
|
346
376
|
"react-native": "./src/initializers/index.ts",
|
|
@@ -598,7 +628,6 @@
|
|
|
598
628
|
},
|
|
599
629
|
"peerDependencies": {
|
|
600
630
|
"@babel/eslint-parser": "^7.19.1",
|
|
601
|
-
"@bigbinary/app-version-control": "^1.1.6",
|
|
602
631
|
"@bigbinary/neetoui-rn": ">=3.0.0",
|
|
603
632
|
"@eslint/js": "^9.26.0",
|
|
604
633
|
"@gorhom/bottom-sheet": ">=5.0.0",
|
|
@@ -652,7 +681,7 @@
|
|
|
652
681
|
"@rozenite/redux-devtools-plugin": "2.1.0",
|
|
653
682
|
"@rozenite/storage-plugin": "2.1.0",
|
|
654
683
|
"@rozenite/tanstack-query-plugin": "2.1.0",
|
|
655
|
-
"compare-versions": "^6.
|
|
684
|
+
"compare-versions": "^6.1.1",
|
|
656
685
|
"formik": "^2.2.9",
|
|
657
686
|
"i18next": "^26.3.6",
|
|
658
687
|
"qs": "^6.15.2",
|
|
@@ -670,7 +699,6 @@
|
|
|
670
699
|
"@babel/core": "^7.29.6",
|
|
671
700
|
"@babel/eslint-parser": "^7.19.1",
|
|
672
701
|
"@babel/runtime": "^7.12.5",
|
|
673
|
-
"@bigbinary/app-version-control": "^1.1.6",
|
|
674
702
|
"@bigbinary/neetoui-rn": "^3.0.0",
|
|
675
703
|
"@bugsnag/source-maps": "^2.3.1",
|
|
676
704
|
"@eslint/js": "^9.26.0",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useRequestNotificationPermission } from "../hooks/useRequestNotificationPermission";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Raises the notification permission prompt, once, and renders nothing.
|
|
5
|
+
*
|
|
6
|
+
* Mount it inside the signed-in branch of the navigator:
|
|
7
|
+
*
|
|
8
|
+
* {isLoggedIn && <PushPermissionGate />}
|
|
9
|
+
*
|
|
10
|
+
* A component rather than a hook call because the request happens as soon as
|
|
11
|
+
* it runs and hooks cannot be called conditionally -- mounting is what gates
|
|
12
|
+
* it on auth, and what keeps the prompt off the login screen. Unmounting on
|
|
13
|
+
* sign-out means the next account on a shared device is asked on its own
|
|
14
|
+
* first login rather than inheriting the previous session's state.
|
|
15
|
+
*
|
|
16
|
+
* This exists because every app that put the request on its Profile screen
|
|
17
|
+
* shipped the same bug: Profile is a lazily mounted tab, so a user who never
|
|
18
|
+
* opened it was never asked, while the app went on registering a device token
|
|
19
|
+
* for them. See neetozone/neeto-chat-rn#788.
|
|
20
|
+
*/
|
|
21
|
+
export const PushPermissionGate = () => {
|
|
22
|
+
useRequestNotificationPermission();
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import AppleLogo from "../assets/images/apple-logo.svg";
|
|
13
13
|
import GoogleLogo from "../assets/images/google-logo.svg";
|
|
14
|
+
import { AUTH_BUTTON_HEIGHT } from "../constants";
|
|
14
15
|
import { moderateScale } from "../helpers/scale";
|
|
15
16
|
|
|
16
17
|
// neetoui-rn v2 shipped this; v3 dropped it. Reproduced here against the v3
|
|
@@ -68,7 +69,7 @@ const styles = StyleSheet.create({
|
|
|
68
69
|
borderColor: lightTheme.colors.palette.grey800,
|
|
69
70
|
borderRadius: moderateScale(8),
|
|
70
71
|
borderWidth: moderateScale(1),
|
|
71
|
-
height: moderateScale(
|
|
72
|
+
height: moderateScale(AUTH_BUTTON_HEIGHT),
|
|
72
73
|
width: "100%",
|
|
73
74
|
},
|
|
74
75
|
logo: {
|
package/src/components/index.ts
CHANGED
package/src/config/bugsnag.ts
CHANGED
|
@@ -103,11 +103,18 @@ export const configureBugsnag = () => {
|
|
|
103
103
|
|
|
104
104
|
// Only in production: QA runs release builds on emulators on purpose,
|
|
105
105
|
// and dropping those would hide the very failures they are hunting.
|
|
106
|
-
// Bugsnag's own flag is
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
106
|
+
// Bugsnag's own flag is unreliable — sdk_gphone images self-report
|
|
107
|
+
// `emulator: false` — so also match the well-known emulator model
|
|
108
|
+
// names. Stage separation alone does not cover QA running
|
|
109
|
+
// production-stage builds on emulators.
|
|
110
|
+
const deviceModel = event.device?.model?.toLowerCase() ?? "";
|
|
111
|
+
const isKnownEmulator =
|
|
112
|
+
event.device?.emulator ||
|
|
113
|
+
deviceModel.startsWith("sdk_gphone") ||
|
|
114
|
+
deviceModel.startsWith("generic") ||
|
|
115
|
+
deviceModel.startsWith("emulator") ||
|
|
116
|
+
deviceModel.includes("android sdk built for");
|
|
117
|
+
if (releaseStage === "production" && isKnownEmulator) {
|
|
111
118
|
return false;
|
|
112
119
|
}
|
|
113
120
|
|
package/src/constants/index.ts
CHANGED
|
@@ -4,6 +4,16 @@ export const STORAGE_KEYS = {
|
|
|
4
4
|
DEVICE_TOKEN: "devicetoken",
|
|
5
5
|
IS_VISITED: "isVisited",
|
|
6
6
|
NOTIFICATION_PERMISSION_REQUESTED: "notificationPermissionRequested",
|
|
7
|
+
/**
|
|
8
|
+
* Read-only fallback. Before this key existed here, every app kept its own
|
|
9
|
+
* "have we asked yet?" flag under this exact string -- neeto-desk,
|
|
10
|
+
* neeto-cal, neeto-form and neeto-invoice all wrote it verbatim from their
|
|
11
|
+
* own constants. An app adopting the hooks here would otherwise start with
|
|
12
|
+
* a null flag and re-prompt everyone who had already declined, so the
|
|
13
|
+
* request path reads this when its own key is unset. Never written; drop it
|
|
14
|
+
* once installs have rolled over.
|
|
15
|
+
*/
|
|
16
|
+
LEGACY_APP_NOTIFICATION_PROMPT_DONE: "notification_first_prompt_done",
|
|
7
17
|
} as const;
|
|
8
18
|
|
|
9
19
|
export const DEFAULT_PAGE_SIZE = 30;
|
|
@@ -18,5 +28,7 @@ export const APP_VERSION_ERROR_REPORT_INTERVAL = 60 * 60 * 1000;
|
|
|
18
28
|
/** One Bugsnag group for every app_versions failure, whatever the status. */
|
|
19
29
|
export const APP_VERSION_ERROR_GROUPING_HASH = "app-version-check-failed";
|
|
20
30
|
|
|
31
|
+
export const AUTH_BUTTON_HEIGHT = 48;
|
|
32
|
+
|
|
21
33
|
/** Design baseline: a ~5" phone, the width every scaled size was drawn against. */
|
|
22
34
|
export const GUIDELINE_BASE_WIDTH = 350;
|
|
@@ -2,3 +2,7 @@ export { usePushNotification } from "./usePushNotification";
|
|
|
2
2
|
export { registerBackgroundHandlers } from "./registerBackgroundHandlers";
|
|
3
3
|
export { NOTIFICATION_CHANNELS, NOTIFICATION_CHANNEL_IDS } from "./constants";
|
|
4
4
|
export { getInitialFirebaseNotification } from "./firebaseMessaging";
|
|
5
|
+
export {
|
|
6
|
+
hasBeenPromptedForNotifications,
|
|
7
|
+
needsNotificationPermissionPrompt,
|
|
8
|
+
} from "./permission";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
import { STORAGE_KEYS } from "../../constants";
|
|
4
|
+
import { getItem } from "../asyncStore";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether this device has a notification permission worth asking for.
|
|
8
|
+
*
|
|
9
|
+
* Android only gained a runtime notification permission in 13 (API 33);
|
|
10
|
+
* below that the manifest entry is the whole story and there is nothing to
|
|
11
|
+
* prompt for. Every iOS version needs asking.
|
|
12
|
+
*
|
|
13
|
+
* Consuming apps used to hand-roll this as
|
|
14
|
+
* `Platform.OS !== "android" || Platform.Version < 33`, which returns early
|
|
15
|
+
* on iOS as well and so never asked there at all. neeto-desk shipped that
|
|
16
|
+
* for months; neeto-form and neeto-invoice still carry the same line. Owning
|
|
17
|
+
* it here is the point of this module.
|
|
18
|
+
*
|
|
19
|
+
* Note that `Platform.Version` is a number on Android and a string on iOS,
|
|
20
|
+
* so the version comparison is only meaningful once the OS is known to be
|
|
21
|
+
* Android. Coercing keeps a string version from silently comparing as NaN.
|
|
22
|
+
*
|
|
23
|
+
* Parameters are injectable so the platform matrix is testable without
|
|
24
|
+
* mutating the Platform module.
|
|
25
|
+
*/
|
|
26
|
+
export const needsNotificationPermissionPrompt = (
|
|
27
|
+
os: typeof Platform.OS = Platform.OS,
|
|
28
|
+
version: typeof Platform.Version = Platform.Version
|
|
29
|
+
) => os !== "android" || Number(version) >= 33;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Has this install already been through the prompt?
|
|
33
|
+
*
|
|
34
|
+
* Falls back to the app-local flag every consuming app used to keep under
|
|
35
|
+
* "notification_first_prompt_done". Without that read, an app adopting these
|
|
36
|
+
* hooks starts with a null flag, decides nobody has been asked, and
|
|
37
|
+
* re-prompts everyone whose status is still DENIED -- on Android 13+ that is
|
|
38
|
+
* every user who declined exactly once, since Android allows a second ask.
|
|
39
|
+
* iOS is unaffected either way: a prior denial reads BLOCKED, not DENIED.
|
|
40
|
+
*
|
|
41
|
+
* A fallback read rather than a migration, on purpose. A migration would have
|
|
42
|
+
* to finish before the requesting hook mounts -- it prompts on mount -- which
|
|
43
|
+
* means gating the gate on an async seed. Reading both keys has no ordering
|
|
44
|
+
* to get wrong, is idempotent, and covers every app at once because they all
|
|
45
|
+
* wrote the identical string.
|
|
46
|
+
*/
|
|
47
|
+
export const hasBeenPromptedForNotifications = async () => {
|
|
48
|
+
const current = await getItem(STORAGE_KEYS.NOTIFICATION_PERMISSION_REQUESTED);
|
|
49
|
+
if (current !== null) return true;
|
|
50
|
+
|
|
51
|
+
const legacy = await getItem(
|
|
52
|
+
STORAGE_KEYS.LEGACY_APP_NOTIFICATION_PROMPT_DONE
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return legacy !== null;
|
|
56
|
+
};
|
|
@@ -21,18 +21,29 @@ export const registerBackgroundHandlers = (
|
|
|
21
21
|
// Data-only messages need manual display; notification-type messages
|
|
22
22
|
// are displayed by the OS automatically.
|
|
23
23
|
if (remoteMessage.data && !remoteMessage.notification) {
|
|
24
|
+
// Non-string values fail notifee's data validation — demo pushes from
|
|
25
|
+
// tower carry no url, for instance.
|
|
26
|
+
const data = Object.fromEntries(
|
|
27
|
+
Object.entries(remoteMessage.data).filter(
|
|
28
|
+
([, value]) => typeof value === "string"
|
|
29
|
+
)
|
|
30
|
+
) as Record<string, string>;
|
|
31
|
+
|
|
32
|
+
// MIUI and other OEM launchers read the numeric badge off the active
|
|
33
|
+
// notification (setNumber), not from any badge API.
|
|
34
|
+
const unreadCount = Number(data.unread_count);
|
|
35
|
+
|
|
24
36
|
await notifee.displayNotification({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
body:
|
|
29
|
-
(remoteMessage.data.message as string) || "You have a new message",
|
|
30
|
-
data: remoteMessage.data,
|
|
37
|
+
title: data.title || "New Notification",
|
|
38
|
+
body: data.message || "You have a new message",
|
|
39
|
+
data,
|
|
31
40
|
android: {
|
|
32
41
|
channelId: "default",
|
|
33
42
|
smallIcon: "ic_notification",
|
|
34
43
|
sound: "default",
|
|
35
44
|
pressAction: { id: "default" },
|
|
45
|
+
...(Number.isFinite(unreadCount) &&
|
|
46
|
+
unreadCount > 0 && { badgeCount: unreadCount }),
|
|
36
47
|
},
|
|
37
48
|
});
|
|
38
49
|
}
|
|
@@ -127,30 +127,37 @@ export const usePushNotification = (
|
|
|
127
127
|
if (Platform.OS !== "android") return undefined;
|
|
128
128
|
|
|
129
129
|
const unsubscribe = messaging().onMessage(async remoteMessage => {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
130
|
+
// Non-string values (or a missing key sent as undefined) fail notifee's
|
|
131
|
+
// data validation — demo pushes from tower carry no url, for instance.
|
|
132
|
+
const data = Object.fromEntries(
|
|
133
|
+
Object.entries(remoteMessage.data ?? {}).filter(
|
|
134
|
+
([, value]) => typeof value === "string"
|
|
135
|
+
)
|
|
136
|
+
) as Record<string, string>;
|
|
137
|
+
|
|
132
138
|
const title =
|
|
133
|
-
remoteMessage.notification?.title ||
|
|
134
|
-
(remoteMessage.data?.title as string) ||
|
|
135
|
-
"New Notification";
|
|
139
|
+
remoteMessage.notification?.title || data.title || "New Notification";
|
|
136
140
|
|
|
137
141
|
const body =
|
|
138
142
|
remoteMessage.notification?.body ||
|
|
139
|
-
|
|
143
|
+
data.message ||
|
|
140
144
|
"You have a new message";
|
|
141
145
|
|
|
146
|
+
// MIUI and other OEM launchers read the numeric badge off the active
|
|
147
|
+
// notification (setNumber), not from any badge API.
|
|
148
|
+
const unreadCount = Number(data.unread_count);
|
|
149
|
+
|
|
142
150
|
await notifee.displayNotification({
|
|
143
151
|
title,
|
|
144
152
|
body,
|
|
145
|
-
data
|
|
146
|
-
string,
|
|
147
|
-
string | number | object
|
|
148
|
-
>,
|
|
153
|
+
data,
|
|
149
154
|
android: {
|
|
150
155
|
channelId: "default",
|
|
151
156
|
smallIcon: "ic_notification",
|
|
152
157
|
sound: "default",
|
|
153
158
|
pressAction: { id: "default" },
|
|
159
|
+
...(Number.isFinite(unreadCount) &&
|
|
160
|
+
unreadCount > 0 && { badgeCount: unreadCount }),
|
|
154
161
|
},
|
|
155
162
|
});
|
|
156
163
|
});
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { useLocalization } from "../contexts/localization";
|
|
2
2
|
export { usePushNotificationPermissions } from "./usePushNotificationPermissions";
|
|
3
|
+
export { useNotificationPermissionStatus } from "./useNotificationPermissionStatus";
|
|
4
|
+
export { useRequestNotificationPermission } from "./useRequestNotificationPermission";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { AppState } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { RESULTS, checkNotifications } from "react-native-permissions";
|
|
5
|
+
|
|
6
|
+
import { needsNotificationPermissionPrompt } from "../helpers/notifications/permission";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Read-only view of the notification permission.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately never requests. A screen that merely displays whether
|
|
12
|
+
* notifications are on -- every app's Profile -- must not be the thing that
|
|
13
|
+
* asks, or users who never open it are never asked at all. That is exactly
|
|
14
|
+
* what usePushNotificationPermissions caused by combining the two: it was
|
|
15
|
+
* called from Profile because Profile is where the status is shown, which
|
|
16
|
+
* made a settings screen the app's only requester. Use PushPermissionGate
|
|
17
|
+
* for the asking.
|
|
18
|
+
*
|
|
19
|
+
* Re-checks on foreground, because the only way back from a denial is the
|
|
20
|
+
* system Settings app, which means leaving the app and returning.
|
|
21
|
+
*/
|
|
22
|
+
export const useNotificationPermissionStatus = () => {
|
|
23
|
+
const [isGranted, setIsGranted] = useState(false);
|
|
24
|
+
|
|
25
|
+
const check = useCallback(async () => {
|
|
26
|
+
// Below Android 13 there is no runtime permission to be missing, so the
|
|
27
|
+
// honest answer is "granted" rather than whatever checkNotifications
|
|
28
|
+
// reports.
|
|
29
|
+
if (!needsNotificationPermissionPrompt()) {
|
|
30
|
+
setIsGranted(true);
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { status } = await checkNotifications();
|
|
36
|
+
setIsGranted(status === RESULTS.GRANTED);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
check();
|
|
41
|
+
|
|
42
|
+
const subscription = AppState.addEventListener("change", nextState => {
|
|
43
|
+
if (nextState === "active") check();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return () => subscription.remove();
|
|
47
|
+
}, [check]);
|
|
48
|
+
|
|
49
|
+
return isGranted;
|
|
50
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
|
-
import { AppState
|
|
2
|
+
import { AppState } from "react-native";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
RESULTS,
|
|
@@ -10,8 +10,20 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import { bugsnagErrorHandler, toNotifiableError } from "../config/bugsnag";
|
|
12
12
|
import { STORAGE_KEYS } from "../constants";
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import { setItem } from "../helpers/asyncStore";
|
|
14
|
+
import {
|
|
15
|
+
hasBeenPromptedForNotifications,
|
|
16
|
+
needsNotificationPermissionPrompt,
|
|
17
|
+
} from "../helpers/notifications/permission";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Checks and requests in one call, which is why every app that
|
|
21
|
+
* wanted to *display* the status ended up making its Profile screen the only
|
|
22
|
+
* thing that ever *asked* -- see neetozone/neeto-chat-rn#788. Prefer
|
|
23
|
+
* `useNotificationPermissionStatus()` to read and `<PushPermissionGate />`
|
|
24
|
+
* (or `useRequestNotificationPermission()`) to ask. Kept working for
|
|
25
|
+
* consumers that have not moved yet.
|
|
26
|
+
*/
|
|
15
27
|
export const usePushNotificationPermissions = () => {
|
|
16
28
|
const [appState, setAppState] = useState(AppState.currentState);
|
|
17
29
|
const [notificationPermission, setNotificationPermission] = useState(false);
|
|
@@ -31,7 +43,7 @@ export const usePushNotificationPermissions = () => {
|
|
|
31
43
|
status?: PermissionStatus;
|
|
32
44
|
}> => {
|
|
33
45
|
try {
|
|
34
|
-
if (
|
|
46
|
+
if (!needsNotificationPermissionPrompt()) {
|
|
35
47
|
return { isGranted: true };
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -50,11 +62,10 @@ export const usePushNotificationPermissions = () => {
|
|
|
50
62
|
|
|
51
63
|
const requestNotificationPermission = useCallback(async () => {
|
|
52
64
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (permissionRequested !== null) {
|
|
65
|
+
// Reads the app-local legacy flag too, so an app that has been
|
|
66
|
+
// writing "notification_first_prompt_done" does not re-prompt everyone
|
|
67
|
+
// who already declined.
|
|
68
|
+
if (await hasBeenPromptedForNotifications()) {
|
|
58
69
|
return false;
|
|
59
70
|
}
|
|
60
71
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
RESULTS,
|
|
5
|
+
checkNotifications,
|
|
6
|
+
requestNotifications,
|
|
7
|
+
} from "react-native-permissions";
|
|
8
|
+
|
|
9
|
+
import { bugsnagErrorHandler, toNotifiableError } from "../config/bugsnag";
|
|
10
|
+
import { STORAGE_KEYS } from "../constants";
|
|
11
|
+
import { setItem } from "../helpers/asyncStore";
|
|
12
|
+
import {
|
|
13
|
+
hasBeenPromptedForNotifications,
|
|
14
|
+
needsNotificationPermissionPrompt,
|
|
15
|
+
} from "../helpers/notifications/permission";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Asks for notification permission once per install, and nothing else.
|
|
19
|
+
*
|
|
20
|
+
* Prefer mounting PushPermissionGate inside the signed-in branch of the
|
|
21
|
+
* navigator; this hook is what it wraps, exported for apps that need to
|
|
22
|
+
* trigger the request from somewhere other than a mount.
|
|
23
|
+
*
|
|
24
|
+
* Only asks when the status is DENIED. GRANTED needs nothing and BLOCKED
|
|
25
|
+
* cannot be reopened from inside the app -- sending a blocked user to
|
|
26
|
+
* Settings is denied-state recovery, which belongs to the UI, not here.
|
|
27
|
+
*/
|
|
28
|
+
export const useRequestNotificationPermission = (enabled = true) => {
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const request = async () => {
|
|
31
|
+
try {
|
|
32
|
+
if (!enabled || !needsNotificationPermissionPrompt()) return;
|
|
33
|
+
|
|
34
|
+
if (await hasBeenPromptedForNotifications()) return;
|
|
35
|
+
|
|
36
|
+
const { status } = await checkNotifications();
|
|
37
|
+
if (status !== RESULTS.DENIED) return;
|
|
38
|
+
|
|
39
|
+
const result = await requestNotifications(["alert", "sound", "badge"]);
|
|
40
|
+
|
|
41
|
+
await setItem(
|
|
42
|
+
STORAGE_KEYS.NOTIFICATION_PERMISSION_REQUESTED,
|
|
43
|
+
result.status === RESULTS.GRANTED
|
|
44
|
+
);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Written even on failure: without it a throwing request retries on
|
|
47
|
+
// every foreground, which is a prompt loop rather than one prompt.
|
|
48
|
+
await setItem(STORAGE_KEYS.NOTIFICATION_PERMISSION_REQUESTED, false);
|
|
49
|
+
bugsnagErrorHandler(toNotifiableError(error), "notification");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
request();
|
|
54
|
+
}, [enabled]);
|
|
55
|
+
};
|
|
@@ -68,14 +68,17 @@ export const NeetoCommonsRNProvider = ({
|
|
|
68
68
|
}: NeetoCommonsRNProviderProps) => {
|
|
69
69
|
const prevReducersRef = useRef<Record<string, Reducer> | null>(null);
|
|
70
70
|
const prevBlacklistRef = useRef<string[] | undefined>(undefined);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
if (
|
|
74
|
+
prevReducersRef.current !== hostRootReducer ||
|
|
75
|
+
prevBlacklistRef.current !== blacklistedReducers
|
|
76
|
+
) {
|
|
77
|
+
prevReducersRef.current = hostRootReducer;
|
|
78
|
+
prevBlacklistRef.current = blacklistedReducers;
|
|
79
|
+
updateRootReducer({ reducers: hostRootReducer, blacklistedReducers });
|
|
80
|
+
}
|
|
81
|
+
}, [hostRootReducer, blacklistedReducers]);
|
|
79
82
|
|
|
80
83
|
useLayoutEffect(() => {
|
|
81
84
|
setHostNavigationRef(hostNavigationRef);
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
|
|
25
25
|
import { SocialButton } from "../../components/SocialButton";
|
|
26
26
|
import { bugsnagErrorHandler } from "../../config/bugsnag";
|
|
27
|
+
import { AUTH_BUTTON_HEIGHT } from "../../constants";
|
|
27
28
|
import { useLocalization } from "../../contexts/localization";
|
|
28
29
|
import { removeSecureValue } from "../../helpers/keychain";
|
|
29
30
|
import { moderateScale } from "../../helpers/scale";
|
|
@@ -196,6 +197,7 @@ export const AuthOptions = ({
|
|
|
196
197
|
<Button
|
|
197
198
|
isLoading={buttonLoading}
|
|
198
199
|
label={strings.commons.buttons.continue}
|
|
200
|
+
style={styles.primaryButton}
|
|
199
201
|
onPress={() => handleOnLogin("email")}
|
|
200
202
|
/>
|
|
201
203
|
<Container style={styles.separatorRow}>
|
|
@@ -235,6 +237,9 @@ export const AuthOptions = ({
|
|
|
235
237
|
};
|
|
236
238
|
|
|
237
239
|
const styles = StyleSheet.create({
|
|
240
|
+
primaryButton: {
|
|
241
|
+
height: moderateScale(AUTH_BUTTON_HEIGHT),
|
|
242
|
+
},
|
|
238
243
|
appleButton: {
|
|
239
244
|
marginTop: moderateScale(24),
|
|
240
245
|
},
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
} from "@bigbinary/neetoui-rn";
|
|
14
14
|
import { useFocusEffect } from "@react-navigation/native";
|
|
15
15
|
|
|
16
|
+
import { AUTH_BUTTON_HEIGHT } from "../../constants";
|
|
16
17
|
import { useLocalization } from "../../contexts/localization";
|
|
18
|
+
import { moderateScale } from "../../helpers/scale";
|
|
17
19
|
import { useAuth } from "../../hooks/query/useAuth";
|
|
18
20
|
import { type CommonsScreenProps } from "../../navigation/types";
|
|
19
21
|
|
|
@@ -151,6 +153,7 @@ export const OTPVerificationScreen = ({
|
|
|
151
153
|
<Button
|
|
152
154
|
disabled={Boolean(resendTimer)}
|
|
153
155
|
isLoading={isGeneratingOTP}
|
|
156
|
+
style={styles.authButton}
|
|
154
157
|
variant="text"
|
|
155
158
|
// `formatString` returns a plain string whenever every argument is
|
|
156
159
|
// one, which is the case here.
|
|
@@ -172,6 +175,7 @@ export const OTPVerificationScreen = ({
|
|
|
172
175
|
disabled={code.length !== 6}
|
|
173
176
|
isLoading={isVerifyingOTP}
|
|
174
177
|
label={strings.commons.buttons.continue}
|
|
178
|
+
style={styles.authButton}
|
|
175
179
|
onPress={() => verifyOTP(code)}
|
|
176
180
|
/>
|
|
177
181
|
</Container>
|
|
@@ -181,6 +185,9 @@ export const OTPVerificationScreen = ({
|
|
|
181
185
|
};
|
|
182
186
|
|
|
183
187
|
const styles = StyleSheet.create({
|
|
188
|
+
authButton: {
|
|
189
|
+
height: moderateScale(AUTH_BUTTON_HEIGHT),
|
|
190
|
+
},
|
|
184
191
|
screen: {
|
|
185
192
|
flex: 1,
|
|
186
193
|
},
|