pushwave-client 0.3.5 → 0.3.7

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 CHANGED
@@ -24,6 +24,16 @@ npm install pushwave-client
24
24
  npx expo install expo-notifications
25
25
  ```
26
26
 
27
+ Optional (recommended): install [expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/) to persist the SDK’s installationId across app restarts:
28
+ ```bash
29
+ npx expo install expo-secure-store
30
+ ```
31
+
32
+ Optional (for richer device metadata: version/build, model, locale/timezone): install:
33
+ ```bash
34
+ npx expo install expo-application expo-device expo-localization
35
+ ```
36
+
27
37
  2) Add the config plugin to your app.json / app.config.*:
28
38
  ```json
29
39
  {
@@ -109,4 +119,4 @@ export default function App() {
109
119
 
110
120
  ---
111
121
 
112
- *Roadmap is no longer tracked in this README; check the dashboard or docs for current feature status.*
122
+ *Roadmap is no longer tracked in this README; check the dashboard or docs for current feature status.*
@@ -11,4 +11,12 @@ export interface RegisterPushWaveDTO {
11
11
  platform: string;
12
12
  appAttestation?: any;
13
13
  environment: "development" | "production";
14
+ appVersion: string;
15
+ buildNumber: string;
16
+ installationId: string;
17
+ osVersion: string;
18
+ deviceModel: string;
19
+ locale: string;
20
+ timezone: string;
21
+ countryCode: string;
14
22
  }
@@ -7,6 +7,8 @@ const pwLogger_1 = require("../utils/pwLogger");
7
7
  const apiKeyCheck_1 = require("../utils/apiKeyCheck");
8
8
  const index_1 = require("../attestation/index");
9
9
  const fetch_1 = require("../utils/fetch");
10
+ const installationId_1 = require("../utils/installationId");
11
+ const collectDeviceMetaData_1 = require("../utils/collectDeviceMetaData");
10
12
  async function registerPushWave({ apiKey }) {
11
13
  const OS = react_native_1.Platform.OS;
12
14
  if ((0, apiKeyCheck_1.isSecretKey)(apiKey)) {
@@ -26,12 +28,22 @@ async function registerPushWave({ apiKey }) {
26
28
  if (appAttestation.status === "disabled")
27
29
  pwLogger_1.PWLogger.warn(`(${react_native_1.Platform.OS}) could not get attestation: ${appAttestation.reason}`);
28
30
  const path = "expo-tokens";
31
+ const installationId = await (0, installationId_1.getInstallationId)();
32
+ const { appVersion, buildNumber, countryCode, deviceModel, locale, osVersion, timezone } = (0, collectDeviceMetaData_1.collectDeviceMetaData)();
29
33
  const options = {
30
34
  apiKey: apiKey,
31
35
  expoToken: expoToken,
32
36
  platform: OS,
33
37
  appAttestation: appAttestation,
34
- environment: __DEV__ ? "development" : "production"
38
+ environment: __DEV__ ? "development" : "production",
39
+ installationId,
40
+ appVersion,
41
+ buildNumber,
42
+ countryCode,
43
+ deviceModel,
44
+ locale,
45
+ osVersion,
46
+ timezone
35
47
  };
36
48
  try {
37
49
  const res = await (0, fetch_1.fetchApi)("PUT", path, { data: options });
@@ -0,0 +1,10 @@
1
+ export type DeviceMetaData = {
2
+ appVersion: string;
3
+ buildNumber: string;
4
+ countryCode: string;
5
+ deviceModel: string;
6
+ locale: string;
7
+ osVersion: string;
8
+ timezone: string;
9
+ };
10
+ export declare const collectDeviceMetaData: () => DeviceMetaData;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectDeviceMetaData = void 0;
7
+ const react_native_1 = require("react-native");
8
+ const expo_constants_1 = __importDefault(require("expo-constants"));
9
+ const loadApplication = () => {
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ return require("expo-application");
13
+ }
14
+ catch (err) {
15
+ return null;
16
+ }
17
+ };
18
+ const loadDevice = () => {
19
+ try {
20
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
21
+ return require("expo-device");
22
+ }
23
+ catch (err) {
24
+ return null;
25
+ }
26
+ };
27
+ const loadLocalization = () => {
28
+ try {
29
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
30
+ return require("expo-localization");
31
+ }
32
+ catch (err) {
33
+ return null;
34
+ }
35
+ };
36
+ const collectDeviceMetaData = () => {
37
+ const application = loadApplication();
38
+ const device = loadDevice();
39
+ const localization = loadLocalization();
40
+ const appVersion = application?.nativeApplicationVersion ??
41
+ (expo_constants_1.default.expoConfig?.version ?? "");
42
+ const buildNumber = application?.nativeBuildVersion ??
43
+ (expo_constants_1.default.expoConfig?.ios?.buildNumber ??
44
+ (expo_constants_1.default.expoConfig?.android?.versionCode
45
+ ? String(expo_constants_1.default.expoConfig?.android?.versionCode)
46
+ : ""));
47
+ const deviceModel = device?.modelName ?? (expo_constants_1.default.deviceName ?? "");
48
+ const osVersion = device?.osVersion ?? (react_native_1.Platform.Version ? String(react_native_1.Platform.Version) : "");
49
+ const locales = localization?.getLocales?.() ?? [];
50
+ const primaryLocale = locales[0];
51
+ const locale = primaryLocale?.languageTag ??
52
+ (typeof Intl !== "undefined"
53
+ ? Intl.DateTimeFormat().resolvedOptions().locale
54
+ : "");
55
+ const countryCode = primaryLocale?.regionCode ??
56
+ (() => {
57
+ const parts = locale?.split?.("-") ?? [];
58
+ return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : "";
59
+ })();
60
+ const calendars = localization?.getCalendars?.() ?? [];
61
+ const timezone = calendars[0]?.timeZone ??
62
+ (typeof Intl !== "undefined"
63
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""
64
+ : "");
65
+ return {
66
+ appVersion: appVersion ?? "",
67
+ buildNumber: buildNumber ?? "",
68
+ countryCode: countryCode ?? "",
69
+ deviceModel: deviceModel ?? "",
70
+ locale: locale ?? "",
71
+ osVersion: osVersion ?? "",
72
+ timezone: timezone ?? ""
73
+ };
74
+ };
75
+ exports.collectDeviceMetaData = collectDeviceMetaData;
@@ -11,7 +11,6 @@ async function fetchApi(method, path, { params, data } = {}) {
11
11
  return acc;
12
12
  }, {})).toString()
13
13
  : "";
14
- console.log("Go fetch !");
15
14
  const url = BASE_URL + path + (search ? `?${search}` : "");
16
15
  const headers = {};
17
16
  const body = data !== undefined && method !== "GET" ? JSON.stringify(data) : undefined;
@@ -0,0 +1 @@
1
+ export declare const getInstallationId: () => Promise<string>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getInstallationId = void 0;
4
+ const pwLogger_1 = require("./pwLogger");
5
+ const STORAGE_KEY = "pushwave-installation-id";
6
+ let cachedId = null;
7
+ let warnedMissingSecureStore = false;
8
+ const generateId = () => {
9
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
10
+ return crypto.randomUUID();
11
+ }
12
+ // Fallback UUID-ish generator
13
+ const bytes = new Uint8Array(16);
14
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
15
+ crypto.getRandomValues(bytes);
16
+ }
17
+ else {
18
+ for (let i = 0; i < bytes.length; i++) {
19
+ bytes[i] = Math.floor(Math.random() * 256);
20
+ }
21
+ }
22
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
23
+ return [
24
+ hex.slice(0, 8),
25
+ hex.slice(8, 12),
26
+ hex.slice(12, 16),
27
+ hex.slice(16, 20),
28
+ hex.slice(20)
29
+ ].join("-");
30
+ };
31
+ const loadSecureStore = () => {
32
+ try {
33
+ return require("expo-secure-store");
34
+ }
35
+ catch (err) {
36
+ return null;
37
+ }
38
+ };
39
+ const getInstallationId = async () => {
40
+ if (cachedId)
41
+ return cachedId;
42
+ const SecureStore = loadSecureStore();
43
+ if (SecureStore) {
44
+ try {
45
+ const existing = await SecureStore.getItemAsync(STORAGE_KEY);
46
+ if (existing) {
47
+ cachedId = existing;
48
+ return existing;
49
+ }
50
+ }
51
+ catch (err) {
52
+ pwLogger_1.PWLogger.warn("Failed reading installationId from SecureStore:", err);
53
+ }
54
+ const id = generateId();
55
+ try {
56
+ await SecureStore.setItemAsync(STORAGE_KEY, id);
57
+ }
58
+ catch (err) {
59
+ pwLogger_1.PWLogger.warn("Failed persisting installationId to SecureStore:", err);
60
+ }
61
+ cachedId = id;
62
+ return id;
63
+ }
64
+ if (!warnedMissingSecureStore) {
65
+ warnedMissingSecureStore = true;
66
+ pwLogger_1.PWLogger.warn("expo-secure-store not installed; installationId will not persist across app restarts. Install expo-secure-store for better targeting.");
67
+ }
68
+ // Non-persistent fallback
69
+ cachedId = generateId();
70
+ return cachedId;
71
+ };
72
+ exports.getInstallationId = getInstallationId;
@@ -9,7 +9,6 @@ const pushwaveSettingsPromise = (async () => {
9
9
  return await (0, fetch_1.fetchApi)("GET", "pushwave-config", { params: { platform: react_native_1.Platform.OS } });
10
10
  }
11
11
  catch (e) {
12
- // log si besoin
13
12
  pwLogger_1.PWLogger.warn(`Unable to load PushWave configuration: ${e}`);
14
13
  return {};
15
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pushwave-client",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "PushWave Client, Expo Push Notifications SaaS SDK",
5
5
  "homepage": "https://github.com/luruk-hai/pushwave-client#readme",
6
6
  "bugs": {
@@ -25,7 +25,25 @@
25
25
  "expo-constants": "*",
26
26
  "expo-notifications": "*",
27
27
  "react-native": "*",
28
- "expo": "*"
28
+ "expo": "*",
29
+ "expo-secure-store": "*",
30
+ "expo-application": "*",
31
+ "expo-device": "*",
32
+ "expo-localization": "*"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "expo-secure-store": {
36
+ "optional": true
37
+ },
38
+ "expo-application": {
39
+ "optional": true
40
+ },
41
+ "expo-device": {
42
+ "optional": true
43
+ },
44
+ "expo-localization": {
45
+ "optional": true
46
+ }
29
47
  },
30
48
  "dependencies": {
31
49
  "cross-fetch": "^4.1.0",
@@ -12,5 +12,13 @@ export interface RegisterPushWaveDTO {
12
12
  expoToken: string;
13
13
  platform: string;
14
14
  appAttestation?: any;
15
- environment: "development" | "production"
15
+ environment: "development" | "production",
16
+ appVersion: string;
17
+ buildNumber: string;
18
+ installationId: string;
19
+ osVersion: string;
20
+ deviceModel: string;
21
+ locale: string;
22
+ timezone: string;
23
+ countryCode: string;
16
24
  }
@@ -5,6 +5,8 @@ import { isSecretKey } from "../utils/apiKeyCheck";
5
5
  import { RegisterPushWaveClient, RegisterPushWaveDTO, RegisterPushWaveResponse } from "./registerPushWave.dto";
6
6
  import { getApplicationAttestation } from "../attestation/index";
7
7
  import { fetchApi } from "../utils/fetch";
8
+ import { getInstallationId } from "../utils/installationId";
9
+ import { collectDeviceMetaData } from "../utils/collectDeviceMetaData";
8
10
 
9
11
  export async function registerPushWave(
10
12
  { apiKey }: RegisterPushWaveClient
@@ -37,12 +39,24 @@ export async function registerPushWave(
37
39
 
38
40
  const path = "expo-tokens"
39
41
 
42
+ const installationId = await getInstallationId();
43
+
44
+ const { appVersion, buildNumber, countryCode, deviceModel, locale, osVersion, timezone } = collectDeviceMetaData();
45
+
40
46
  const options: RegisterPushWaveDTO = {
41
47
  apiKey: apiKey,
42
48
  expoToken: expoToken,
43
49
  platform: OS,
44
50
  appAttestation: appAttestation,
45
- environment: __DEV__ ? "development" : "production"
51
+ environment: __DEV__ ? "development" : "production",
52
+ installationId,
53
+ appVersion,
54
+ buildNumber,
55
+ countryCode,
56
+ deviceModel,
57
+ locale,
58
+ osVersion,
59
+ timezone
46
60
  }
47
61
 
48
62
  try {
@@ -0,0 +1,111 @@
1
+ import { Platform } from "react-native";
2
+ import Constants from "expo-constants";
3
+
4
+ export type DeviceMetaData = {
5
+ appVersion: string;
6
+ buildNumber: string;
7
+ countryCode: string;
8
+ deviceModel: string;
9
+ locale: string;
10
+ osVersion: string;
11
+ timezone: string;
12
+ };
13
+
14
+ type ApplicationModule = {
15
+ nativeApplicationVersion?: string | null;
16
+ nativeBuildVersion?: string | null;
17
+ };
18
+
19
+ type DeviceModule = {
20
+ modelName?: string | null;
21
+ osVersion?: string | null;
22
+ };
23
+
24
+ type LocalizationModule = {
25
+ getLocales?: () => Array<{
26
+ languageTag?: string;
27
+ regionCode?: string;
28
+ }>;
29
+ getCalendars?: () => Array<{
30
+ timeZone?: string;
31
+ }>;
32
+ };
33
+
34
+ const loadApplication = (): ApplicationModule | null => {
35
+ try {
36
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
37
+ return require("expo-application") as ApplicationModule;
38
+ } catch (err) {
39
+ return null;
40
+ }
41
+ };
42
+
43
+ const loadDevice = (): DeviceModule | null => {
44
+ try {
45
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
46
+ return require("expo-device") as DeviceModule;
47
+ } catch (err) {
48
+ return null;
49
+ }
50
+ };
51
+
52
+ const loadLocalization = (): LocalizationModule | null => {
53
+ try {
54
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
55
+ return require("expo-localization") as LocalizationModule;
56
+ } catch (err) {
57
+ return null;
58
+ }
59
+ };
60
+
61
+ export const collectDeviceMetaData = (): DeviceMetaData => {
62
+ const application = loadApplication();
63
+ const device = loadDevice();
64
+ const localization = loadLocalization();
65
+
66
+ const appVersion =
67
+ application?.nativeApplicationVersion ??
68
+ (Constants.expoConfig?.version ?? "");
69
+
70
+ const buildNumber =
71
+ application?.nativeBuildVersion ??
72
+ (Constants.expoConfig?.ios?.buildNumber ??
73
+ (Constants.expoConfig?.android?.versionCode
74
+ ? String(Constants.expoConfig?.android?.versionCode)
75
+ : ""));
76
+
77
+ const deviceModel = device?.modelName ?? (Constants.deviceName ?? "");
78
+ const osVersion = device?.osVersion ?? (Platform.Version ? String(Platform.Version) : "");
79
+
80
+ const locales = localization?.getLocales?.() ?? [];
81
+ const primaryLocale = locales[0];
82
+ const locale =
83
+ primaryLocale?.languageTag ??
84
+ (typeof Intl !== "undefined"
85
+ ? Intl.DateTimeFormat().resolvedOptions().locale
86
+ : "");
87
+
88
+ const countryCode =
89
+ primaryLocale?.regionCode ??
90
+ (() => {
91
+ const parts = locale?.split?.("-") ?? [];
92
+ return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : "";
93
+ })();
94
+
95
+ const calendars = localization?.getCalendars?.() ?? [];
96
+ const timezone =
97
+ calendars[0]?.timeZone ??
98
+ (typeof Intl !== "undefined"
99
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone ?? ""
100
+ : "");
101
+
102
+ return {
103
+ appVersion: appVersion ?? "",
104
+ buildNumber: buildNumber ?? "",
105
+ countryCode: countryCode ?? "",
106
+ deviceModel: deviceModel ?? "",
107
+ locale: locale ?? "",
108
+ osVersion: osVersion ?? "",
109
+ timezone: timezone ?? ""
110
+ };
111
+ };
@@ -19,8 +19,6 @@ export async function fetchApi<TResponse>(
19
19
  ).toString()
20
20
  : "";
21
21
 
22
- console.log("Go fetch !");
23
-
24
22
  const url = BASE_URL + path + (search ? `?${search}` : "");
25
23
 
26
24
  const headers: Record<string, string> = {};
@@ -0,0 +1,82 @@
1
+ import { PWLogger } from "./pwLogger";
2
+
3
+ const STORAGE_KEY = "pushwave-installation-id";
4
+
5
+ let cachedId: string | null = null;
6
+ let warnedMissingSecureStore = false;
7
+
8
+ type SecureStoreModule = {
9
+ getItemAsync(key: string): Promise<string | null>;
10
+ setItemAsync(key: string, value: string): Promise<void>;
11
+ };
12
+
13
+ const generateId = (): string => {
14
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
15
+ return crypto.randomUUID();
16
+ }
17
+
18
+ // Fallback UUID-ish generator
19
+ const bytes = new Uint8Array(16);
20
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
21
+ crypto.getRandomValues(bytes);
22
+ } else {
23
+ for (let i = 0; i < bytes.length; i++) {
24
+ bytes[i] = Math.floor(Math.random() * 256);
25
+ }
26
+ }
27
+
28
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
29
+ return [
30
+ hex.slice(0, 8),
31
+ hex.slice(8, 12),
32
+ hex.slice(12, 16),
33
+ hex.slice(16, 20),
34
+ hex.slice(20)
35
+ ].join("-");
36
+ };
37
+
38
+ const loadSecureStore = (): SecureStoreModule | null => {
39
+ try {
40
+ return require("expo-secure-store") as SecureStoreModule;
41
+ } catch (err) {
42
+ return null;
43
+ }
44
+ };
45
+
46
+ export const getInstallationId = async (): Promise<string> => {
47
+ if (cachedId) return cachedId;
48
+
49
+ const SecureStore = loadSecureStore();
50
+
51
+ if (SecureStore) {
52
+ try {
53
+ const existing = await SecureStore.getItemAsync(STORAGE_KEY);
54
+ if (existing) {
55
+ cachedId = existing;
56
+ return existing;
57
+ }
58
+ } catch (err) {
59
+ PWLogger.warn("Failed reading installationId from SecureStore:", err);
60
+ }
61
+
62
+ const id = generateId();
63
+ try {
64
+ await SecureStore.setItemAsync(STORAGE_KEY, id);
65
+ } catch (err) {
66
+ PWLogger.warn("Failed persisting installationId to SecureStore:", err);
67
+ }
68
+ cachedId = id;
69
+ return id;
70
+ }
71
+
72
+ if (!warnedMissingSecureStore) {
73
+ warnedMissingSecureStore = true;
74
+ PWLogger.warn(
75
+ "expo-secure-store not installed; installationId will not persist across app restarts. Install expo-secure-store for better targeting."
76
+ );
77
+ }
78
+
79
+ // Non-persistent fallback
80
+ cachedId = generateId();
81
+ return cachedId;
82
+ };
@@ -11,7 +11,6 @@ const pushwaveSettingsPromise: Promise<PushwaveSettings> = (async () => {
11
11
  try {
12
12
  return await fetchApi("GET", "pushwave-config", { params: { platform: Platform.OS } });
13
13
  } catch (e) {
14
- // log si besoin
15
14
  PWLogger.warn(`Unable to load PushWave configuration: ${e}`)
16
15
  return {}
17
16
  }