pushwave-client 0.1.0
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/android/build.gradle +55 -0
- package/android/consumer-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +7 -0
- package/android/src/main/java/com/pushwaveclient/PushwaveAttestationModule.kt +38 -0
- package/android/src/main/java/com/pushwaveclient/PushwaveAttestationPackage.kt +16 -0
- package/dist/attestation/getAndroidSignature.d.ts +0 -0
- package/dist/attestation/getAndroidSignature.js +1 -0
- package/dist/attestation/getApplicationAttestation.d.ts +16 -0
- package/dist/attestation/getApplicationAttestation.js +56 -0
- package/dist/attestation/getApplicationSignature.d.ts +1 -0
- package/dist/attestation/getApplicationSignature.js +9 -0
- package/dist/attestation/getIosDeviceCheck.d.ts +1 -0
- package/dist/attestation/getIosDeviceCheck.js +2 -0
- package/dist/attestation/native.d.ts +2 -0
- package/dist/attestation/native.js +24 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +12 -0
- package/dist/registerPushWave.d.ts +14 -0
- package/dist/registerPushWave.js +58 -0
- package/dist/utils/apiKeyCheck.d.ts +1 -0
- package/dist/utils/apiKeyCheck.js +6 -0
- package/dist/utils/expoToken.d.ts +1 -0
- package/dist/utils/expoToken.js +59 -0
- package/dist/utils/fetch.d.ts +1 -0
- package/dist/utils/fetch.js +28 -0
- package/dist/utils/pwLogger.d.ts +6 -0
- package/dist/utils/pwLogger.js +35 -0
- package/dist/utils/validation.d.ts +0 -0
- package/dist/utils/validation.js +1 -0
- package/ios/PushwaveAttestation.m +11 -0
- package/ios/PushwaveAttestation.swift +37 -0
- package/package.json +39 -0
- package/plugin/index.js +46 -0
- package/pushwave-client.podspec +21 -0
- package/react-native.config.js +14 -0
- package/src/attestation/getApplicationAttestation.ts +92 -0
- package/src/attestation/native.ts +22 -0
- package/src/index.ts +16 -0
- package/src/registerPushWave.ts +87 -0
- package/src/utils/apiKeyCheck.ts +3 -0
- package/src/utils/expoToken.ts +26 -0
- package/src/utils/fetch.ts +36 -0
- package/src/utils/pwLogger.ts +49 -0
- package/src/utils/validation.ts +0 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
ext.kotlin_version = safeExtGet("kotlinVersion", "1.9.22")
|
|
6
|
+
repositories {
|
|
7
|
+
google()
|
|
8
|
+
mavenCentral()
|
|
9
|
+
}
|
|
10
|
+
dependencies {
|
|
11
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
apply plugin: "com.android.library"
|
|
16
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
17
|
+
|
|
18
|
+
def getExtOrDefault(name, defaultValue) {
|
|
19
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
android {
|
|
23
|
+
namespace "com.pushwaveclient"
|
|
24
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion", 34)
|
|
25
|
+
|
|
26
|
+
defaultConfig {
|
|
27
|
+
minSdkVersion getExtOrDefault("minSdkVersion", 23)
|
|
28
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion", 34)
|
|
29
|
+
consumerProguardFiles "consumer-rules.pro"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
compileOptions {
|
|
33
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
34
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
kotlinOptions {
|
|
38
|
+
jvmTarget = "17"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
lintOptions {
|
|
42
|
+
abortOnError false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
repositories {
|
|
47
|
+
google()
|
|
48
|
+
mavenCentral()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dependencies {
|
|
52
|
+
implementation "com.facebook.react:react-native:+"
|
|
53
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
54
|
+
implementation "com.google.android.play:integrity:1.3.0"
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Keep file intentionally empty; no consumer rules required for this library.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.pushwaveclient
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Promise
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
6
|
+
import com.facebook.react.bridge.ReactMethod
|
|
7
|
+
import com.google.android.gms.tasks.OnFailureListener
|
|
8
|
+
import com.google.android.gms.tasks.OnSuccessListener
|
|
9
|
+
import com.google.android.play.core.integrity.IntegrityManagerFactory
|
|
10
|
+
import com.google.android.play.core.integrity.StandardIntegrityManager
|
|
11
|
+
import com.google.android.play.core.integrity.StandardIntegrityTokenRequest
|
|
12
|
+
|
|
13
|
+
class PushwaveAttestationModule(private val context: ReactApplicationContext) :
|
|
14
|
+
ReactContextBaseJavaModule(context) {
|
|
15
|
+
|
|
16
|
+
override fun getName(): String = "PushwaveAttestation"
|
|
17
|
+
|
|
18
|
+
@ReactMethod
|
|
19
|
+
fun getIntegrityToken(nonce: String, promise: Promise) {
|
|
20
|
+
try {
|
|
21
|
+
val integrityManager = IntegrityManagerFactory.create(context)
|
|
22
|
+
val standard: StandardIntegrityManager = integrityManager.standardIntegrityManager()
|
|
23
|
+
val request = StandardIntegrityTokenRequest.builder()
|
|
24
|
+
.setNonce(nonce)
|
|
25
|
+
.build()
|
|
26
|
+
|
|
27
|
+
standard.requestIntegrityToken(request)
|
|
28
|
+
.addOnSuccessListener(OnSuccessListener { response ->
|
|
29
|
+
promise.resolve(response.token())
|
|
30
|
+
})
|
|
31
|
+
.addOnFailureListener(OnFailureListener { error ->
|
|
32
|
+
promise.reject("PLAY_INTEGRITY_ERROR", error.localizedMessage, error)
|
|
33
|
+
})
|
|
34
|
+
} catch (e: Exception) {
|
|
35
|
+
promise.reject("PLAY_INTEGRITY_ERROR", e.localizedMessage, e)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.pushwaveclient
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class PushwaveAttestationPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
+
return listOf(PushwaveAttestationModule(reactContext))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
14
|
+
return emptyList()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AndroidAttestationPayload {
|
|
2
|
+
nonce: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
integrityToken: string;
|
|
5
|
+
}
|
|
6
|
+
export interface IosAttestationPayload {
|
|
7
|
+
nonce: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
deviceCheckToken: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DisabledAttestation {
|
|
12
|
+
attestationDisabled: true;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export type ApplicationAttestation = AndroidAttestationPayload | IosAttestationPayload | DisabledAttestation | true;
|
|
16
|
+
export default function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = getApplicationAttestation;
|
|
4
|
+
const buffer_1 = require("buffer");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const native_1 = require("./native");
|
|
7
|
+
async function getApplicationAttestation(apiKey) {
|
|
8
|
+
if (!requiresAttestation(apiKey))
|
|
9
|
+
return true;
|
|
10
|
+
const { nonce, timestamp } = createNonce();
|
|
11
|
+
try {
|
|
12
|
+
if (react_native_1.Platform.OS === "android")
|
|
13
|
+
return await getAndroidSignature(nonce, timestamp);
|
|
14
|
+
if (react_native_1.Platform.OS === "ios")
|
|
15
|
+
return await getIosDeviceCheck(nonce, timestamp);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const reason = err?.message ?? "attestation-error";
|
|
19
|
+
return { attestationDisabled: true, reason };
|
|
20
|
+
}
|
|
21
|
+
return { attestationDisabled: true, reason: "platform-unsupported" };
|
|
22
|
+
}
|
|
23
|
+
function requiresAttestation(apiKey) {
|
|
24
|
+
return apiKey.startsWith("pw_pub_");
|
|
25
|
+
}
|
|
26
|
+
function createNonce() {
|
|
27
|
+
const timestamp = Date.now();
|
|
28
|
+
const random = Math.random().toString(36).slice(2);
|
|
29
|
+
const raw = `${timestamp}:${random}`;
|
|
30
|
+
const nonce = base64UrlEncode(raw);
|
|
31
|
+
return { nonce, timestamp };
|
|
32
|
+
}
|
|
33
|
+
function base64UrlEncode(input) {
|
|
34
|
+
if (typeof globalThis.btoa === "function") {
|
|
35
|
+
return globalThis.btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
36
|
+
}
|
|
37
|
+
return buffer_1.Buffer.from(input, "utf8")
|
|
38
|
+
.toString("base64")
|
|
39
|
+
.replace(/\+/g, "-")
|
|
40
|
+
.replace(/\//g, "_")
|
|
41
|
+
.replace(/=+$/, "");
|
|
42
|
+
}
|
|
43
|
+
async function getAndroidSignature(nonce, timestamp) {
|
|
44
|
+
const integrityToken = await (0, native_1.getAndroidIntegrityToken)(nonce);
|
|
45
|
+
if (!integrityToken) {
|
|
46
|
+
return { attestationDisabled: true, reason: "play-integrity-unavailable" };
|
|
47
|
+
}
|
|
48
|
+
return { nonce, timestamp, integrityToken };
|
|
49
|
+
}
|
|
50
|
+
async function getIosDeviceCheck(nonce, timestamp) {
|
|
51
|
+
const deviceCheckToken = await (0, native_1.getDeviceCheckToken)(nonce);
|
|
52
|
+
if (!deviceCheckToken) {
|
|
53
|
+
return { attestationDisabled: true, reason: "devicecheck-unavailable" };
|
|
54
|
+
}
|
|
55
|
+
return { nonce, timestamp, deviceCheckToken };
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function getApplicationSignature(): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function (): any;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAndroidIntegrityToken = getAndroidIntegrityToken;
|
|
4
|
+
exports.getDeviceCheckToken = getDeviceCheckToken;
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const MODULE_NAME = "PushwaveAttestation";
|
|
7
|
+
const NativeAttestation = react_native_1.NativeModules?.[MODULE_NAME];
|
|
8
|
+
function assertLinked() {
|
|
9
|
+
if (!NativeAttestation) {
|
|
10
|
+
throw new Error("native-module-unavailable");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function getAndroidIntegrityToken(nonce) {
|
|
14
|
+
if (react_native_1.Platform.OS !== "android")
|
|
15
|
+
return;
|
|
16
|
+
assertLinked();
|
|
17
|
+
return NativeAttestation.getIntegrityToken(nonce);
|
|
18
|
+
}
|
|
19
|
+
async function getDeviceCheckToken(nonce) {
|
|
20
|
+
if (react_native_1.Platform.OS !== "ios")
|
|
21
|
+
return;
|
|
22
|
+
assertLinked();
|
|
23
|
+
return NativeAttestation.getDeviceCheckToken(nonce);
|
|
24
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { RegisterPushWaveClient, RegisterPushWaveResponse } from "./registerPushWave";
|
|
2
|
+
export interface PushWaveClientType {
|
|
3
|
+
init(options: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
|
|
4
|
+
}
|
|
5
|
+
declare const PushWaveClient: PushWaveClientType;
|
|
6
|
+
export default PushWaveClient;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
const registerPushWave_1 = __importDefault(require("./registerPushWave"));
|
|
7
|
+
const PushWaveClient = {
|
|
8
|
+
init(options) {
|
|
9
|
+
return (0, registerPushWave_1.default)(options);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
exports.default = PushWaveClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RegisterPushWaveClient {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
}
|
|
4
|
+
export interface RegisterPushWaveResponse {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RegisterPushWaveOptions {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
expoToken: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
appAttestation?: any;
|
|
13
|
+
}
|
|
14
|
+
export default function registerPushWave({ apiKey }: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
|
|
@@ -0,0 +1,58 @@
|
|
|
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.default = registerPushWave;
|
|
7
|
+
const fetch_1 = require("./utils/fetch");
|
|
8
|
+
const expoToken_1 = require("./utils/expoToken");
|
|
9
|
+
const getApplicationAttestation_1 = __importDefault(require("./attestation/getApplicationAttestation"));
|
|
10
|
+
const react_native_1 = require("react-native");
|
|
11
|
+
const pwLogger_1 = require("./utils/pwLogger");
|
|
12
|
+
const apiKeyCheck_1 = require("./utils/apiKeyCheck");
|
|
13
|
+
async function registerPushWave({ apiKey }) {
|
|
14
|
+
if ((0, apiKeyCheck_1.isSecretKey)(apiKey)) {
|
|
15
|
+
const warn = `\x1b[0m You are using your SECRET API key in a client environment. This key must NEVER be embedded in a mobile app.`;
|
|
16
|
+
pwLogger_1.PWLogger.warn(warn);
|
|
17
|
+
}
|
|
18
|
+
const expoToken = await (0, expoToken_1.getExpoToken)();
|
|
19
|
+
if (!expoToken) {
|
|
20
|
+
const message = "could not get ExpoToken";
|
|
21
|
+
pwLogger_1.PWLogger.error(message);
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
message: "[PushWaveClient] Error: " + message
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const appAttestation = await (0, getApplicationAttestation_1.default)(apiKey);
|
|
28
|
+
if (!appAttestation) {
|
|
29
|
+
const message = `could not get ${react_native_1.Platform.OS} attestation.`;
|
|
30
|
+
pwLogger_1.PWLogger.error(message);
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
message: `[PushWaveClient] Error: ` + message
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const path = "/v1/expo-tokens";
|
|
37
|
+
const options = {
|
|
38
|
+
apiKey: apiKey,
|
|
39
|
+
expoToken: expoToken,
|
|
40
|
+
platform: react_native_1.Platform.OS,
|
|
41
|
+
appAttestation: appAttestation
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const res = await (0, fetch_1.fetchApi)(path, options);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
message: res.message,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const e = err;
|
|
52
|
+
pwLogger_1.PWLogger.error(e.message);
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
message: e.message,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isSecretKey(apiKey: string): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getExpoToken(): Promise<string | undefined>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.getExpoToken = getExpoToken;
|
|
40
|
+
const Notifications = __importStar(require("expo-notifications"));
|
|
41
|
+
const expo_constants_1 = __importDefault(require("expo-constants"));
|
|
42
|
+
async function getExpoToken() {
|
|
43
|
+
try {
|
|
44
|
+
const existing = await Notifications.getPermissionsAsync();
|
|
45
|
+
const finalStatus = existing.status === "granted"
|
|
46
|
+
? existing.status
|
|
47
|
+
: (await Notifications.requestPermissionsAsync()).status;
|
|
48
|
+
if (finalStatus !== "granted")
|
|
49
|
+
return;
|
|
50
|
+
const projectId = expo_constants_1.default.expoConfig?.extra?.eas?.projectId ?? expo_constants_1.default.easConfig?.projectId;
|
|
51
|
+
const token = (projectId
|
|
52
|
+
? await Notifications.getExpoPushTokenAsync({ projectId })
|
|
53
|
+
: await Notifications.getExpoPushTokenAsync()).data;
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
throw new Error(`[expo-notifications] Error: ` + e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fetchApi<TResponse>(path: string, data: Record<string, any>): Promise<TResponse>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchApi = fetchApi;
|
|
4
|
+
const BASE_URL = "https://pushwave.luruk-hai.fr";
|
|
5
|
+
async function fetchApi(path, data) {
|
|
6
|
+
const url = BASE_URL + path;
|
|
7
|
+
const res = await fetch(url, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
},
|
|
12
|
+
body: JSON.stringify(data),
|
|
13
|
+
});
|
|
14
|
+
const text = await res.text();
|
|
15
|
+
let json = null;
|
|
16
|
+
try {
|
|
17
|
+
json = text ? JSON.parse(text) : null;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
// JSON empty/invalid
|
|
21
|
+
}
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const apiError = (json?.error ?? json?.message ?? "");
|
|
24
|
+
const message = `(${res.status}) ${apiError}`.trim();
|
|
25
|
+
throw new Error(message);
|
|
26
|
+
}
|
|
27
|
+
return json;
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/pwLogger.ts
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PWLogger = void 0;
|
|
5
|
+
const COLORS = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
bold: "\x1b[1m",
|
|
8
|
+
dim: "\x1b[2m",
|
|
9
|
+
red: "\x1b[31m",
|
|
10
|
+
yellow: "\x1b[33m",
|
|
11
|
+
cyan: "\x1b[36m",
|
|
12
|
+
green: "\x1b[32m"
|
|
13
|
+
};
|
|
14
|
+
exports.PWLogger = {
|
|
15
|
+
info: (...args) => {
|
|
16
|
+
if (__DEV__) {
|
|
17
|
+
console.log(`${COLORS.cyan}${COLORS.bold}[PushWave INFO]${COLORS.reset}`, ...args);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
warn: (...args) => {
|
|
21
|
+
if (__DEV__) {
|
|
22
|
+
console.warn(`${COLORS.yellow}${COLORS.bold}⚠️ [PushWave WARNING]${COLORS.reset}`, ...args);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
error: (...args) => {
|
|
26
|
+
if (__DEV__) {
|
|
27
|
+
console.error(`${COLORS.red}${COLORS.bold}❌ [PushWave ERROR]${COLORS.reset}`, ...args);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
success: (...args) => {
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
console.log(`${COLORS.green}${COLORS.bold}✔️ [PushWave]${COLORS.reset}`, ...args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(PushwaveAttestation, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(
|
|
6
|
+
getDeviceCheckToken:(NSString *)nonce
|
|
7
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
8
|
+
rejecter:(RCTPromiseRejectBlock)reject
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
@end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import DeviceCheck
|
|
2
|
+
import Foundation
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
@objc(PushwaveAttestation)
|
|
6
|
+
class PushwaveAttestation: NSObject {
|
|
7
|
+
@objc
|
|
8
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@objc
|
|
13
|
+
func getDeviceCheckToken(
|
|
14
|
+
_ nonce: NSString,
|
|
15
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
16
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
17
|
+
) {
|
|
18
|
+
guard DCDevice.current.isSupported else {
|
|
19
|
+
resolve(NSNull())
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
DCDevice.current.generateToken { data, error in
|
|
24
|
+
if let error = error {
|
|
25
|
+
reject("DEVICE_CHECK_ERROR", error.localizedDescription, error)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
guard let tokenData = data else {
|
|
30
|
+
resolve(NSNull())
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolve(tokenData.base64EncodedString())
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pushwave-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PushWave Client, Expo Push Notifications SaaS SDK",
|
|
5
|
+
"homepage": "https://github.com/luruk-hai/pushwave-client#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/luruk-hai/pushwave-client/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/luruk-hai/pushwave-client.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"expo": {
|
|
18
|
+
"plugin": "./plugin/index.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"clean": "rm -rf dist"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"expo-constants": "*",
|
|
26
|
+
"expo-notifications": "*",
|
|
27
|
+
"react-native": "*",
|
|
28
|
+
"expo": "*"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"cross-fetch": "^4.1.0",
|
|
32
|
+
"@expo/config-plugins": "^8.0.6"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"@types/node": "^24.10.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createRunOncePlugin,
|
|
3
|
+
withAppBuildGradle,
|
|
4
|
+
} = require("@expo/config-plugins");
|
|
5
|
+
|
|
6
|
+
const pkg = require("../package.json");
|
|
7
|
+
|
|
8
|
+
const PLAY_INTEGRITY_DEP = 'implementation("com.google.android.play:integrity:1.3.0")';
|
|
9
|
+
|
|
10
|
+
function withPushwaveAndroid(config) {
|
|
11
|
+
return withAppBuildGradle(config, (config) => {
|
|
12
|
+
if (config.modResults.language !== "groovy") {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const content = config.modResults.contents;
|
|
17
|
+
|
|
18
|
+
if (content.includes(PLAY_INTEGRITY_DEP)) {
|
|
19
|
+
return config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
config.modResults.contents = content.replace(
|
|
23
|
+
/dependencies\s?{[^}]*}/,
|
|
24
|
+
(match) => {
|
|
25
|
+
if (match.includes(PLAY_INTEGRITY_DEP)) return match;
|
|
26
|
+
return match.replace(
|
|
27
|
+
/dependencies\s?{/,
|
|
28
|
+
`dependencies {\n ${PLAY_INTEGRITY_DEP}\n`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return config;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const withPushwaveClient = (config) => {
|
|
38
|
+
config = withPushwaveAndroid(config);
|
|
39
|
+
return config;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = createRunOncePlugin(
|
|
43
|
+
withPushwaveClient,
|
|
44
|
+
pkg.name,
|
|
45
|
+
pkg.version
|
|
46
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "pushwave-client"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"] || "PushWave Client SDK"
|
|
9
|
+
s.homepage = package["homepage"] || "https://github.com/luruk-hai/pushwave-client"
|
|
10
|
+
s.license = package["license"] || "MIT"
|
|
11
|
+
s.author = package["author"] || { "PushWave" => "support@pushwave.dev" }
|
|
12
|
+
s.platforms = { :ios => "13.0" }
|
|
13
|
+
s.source = { :git => package["repository"] ? package["repository"]["url"] : "https://github.com/luruk-hai/pushwave-client.git",
|
|
14
|
+
:tag => "v#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
17
|
+
s.requires_arc = true
|
|
18
|
+
s.swift_version = "5.0"
|
|
19
|
+
s.frameworks = "DeviceCheck"
|
|
20
|
+
s.dependency "React-Core"
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
dependency: {
|
|
3
|
+
platforms: {
|
|
4
|
+
ios: {
|
|
5
|
+
podspecPath: "pushwave-client.podspec",
|
|
6
|
+
},
|
|
7
|
+
android: {
|
|
8
|
+
sourceDir: "android",
|
|
9
|
+
packageImportPath: "import com.pushwaveclient.PushwaveAttestationPackage",
|
|
10
|
+
packageInstance: "new PushwaveAttestationPackage()",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { getAndroidIntegrityToken, getDeviceCheckToken } from "./native";
|
|
4
|
+
|
|
5
|
+
export interface AndroidAttestationPayload {
|
|
6
|
+
nonce: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
integrityToken: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IosAttestationPayload {
|
|
12
|
+
nonce: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
deviceCheckToken: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DisabledAttestation {
|
|
18
|
+
attestationDisabled: true;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ApplicationAttestation =
|
|
23
|
+
| AndroidAttestationPayload
|
|
24
|
+
| IosAttestationPayload
|
|
25
|
+
| DisabledAttestation
|
|
26
|
+
| true;
|
|
27
|
+
|
|
28
|
+
export default async function getApplicationAttestation(apiKey: string): Promise<ApplicationAttestation> {
|
|
29
|
+
if (!requiresAttestation(apiKey)) return true;
|
|
30
|
+
|
|
31
|
+
const { nonce, timestamp } = createNonce();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (Platform.OS === "android") return await getAndroidSignature(nonce, timestamp);
|
|
35
|
+
if (Platform.OS === "ios") return await getIosDeviceCheck(nonce, timestamp);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const reason = (err as Error)?.message ?? "attestation-error";
|
|
38
|
+
return { attestationDisabled: true, reason };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { attestationDisabled: true, reason: "platform-unsupported" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function requiresAttestation(apiKey: string) {
|
|
45
|
+
return apiKey.startsWith("pw_pub_");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createNonce() {
|
|
49
|
+
const timestamp = Date.now();
|
|
50
|
+
const random = Math.random().toString(36).slice(2);
|
|
51
|
+
const raw = `${timestamp}:${random}`;
|
|
52
|
+
const nonce = base64UrlEncode(raw);
|
|
53
|
+
return { nonce, timestamp };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function base64UrlEncode(input: string): string {
|
|
57
|
+
if (typeof globalThis.btoa === "function") {
|
|
58
|
+
return globalThis.btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Buffer.from(input, "utf8")
|
|
62
|
+
.toString("base64")
|
|
63
|
+
.replace(/\+/g, "-")
|
|
64
|
+
.replace(/\//g, "_")
|
|
65
|
+
.replace(/=+$/, "");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function getAndroidSignature(
|
|
69
|
+
nonce: string,
|
|
70
|
+
timestamp: number
|
|
71
|
+
): Promise<AndroidAttestationPayload | DisabledAttestation> {
|
|
72
|
+
const integrityToken = await getAndroidIntegrityToken(nonce);
|
|
73
|
+
|
|
74
|
+
if (!integrityToken) {
|
|
75
|
+
return { attestationDisabled: true, reason: "play-integrity-unavailable" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { nonce, timestamp, integrityToken };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getIosDeviceCheck(
|
|
82
|
+
nonce: string,
|
|
83
|
+
timestamp: number
|
|
84
|
+
): Promise<IosAttestationPayload | DisabledAttestation> {
|
|
85
|
+
const deviceCheckToken = await getDeviceCheckToken(nonce);
|
|
86
|
+
|
|
87
|
+
if (!deviceCheckToken) {
|
|
88
|
+
return { attestationDisabled: true, reason: "devicecheck-unavailable" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { nonce, timestamp, deviceCheckToken };
|
|
92
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NativeModules, Platform } from "react-native";
|
|
2
|
+
|
|
3
|
+
const MODULE_NAME = "PushwaveAttestation";
|
|
4
|
+
const NativeAttestation = NativeModules?.[MODULE_NAME];
|
|
5
|
+
|
|
6
|
+
function assertLinked() {
|
|
7
|
+
if (!NativeAttestation) {
|
|
8
|
+
throw new Error("native-module-unavailable");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getAndroidIntegrityToken(nonce: string): Promise<string | undefined> {
|
|
13
|
+
if (Platform.OS !== "android") return;
|
|
14
|
+
assertLinked();
|
|
15
|
+
return NativeAttestation.getIntegrityToken(nonce);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getDeviceCheckToken(nonce: string): Promise<string | undefined> {
|
|
19
|
+
if (Platform.OS !== "ios") return;
|
|
20
|
+
assertLinked();
|
|
21
|
+
return NativeAttestation.getDeviceCheckToken(nonce);
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import registerPushWave, {
|
|
2
|
+
RegisterPushWaveClient,
|
|
3
|
+
RegisterPushWaveResponse,
|
|
4
|
+
} from "./registerPushWave";
|
|
5
|
+
|
|
6
|
+
export interface PushWaveClientType {
|
|
7
|
+
init(options: RegisterPushWaveClient): Promise<RegisterPushWaveResponse>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const PushWaveClient: PushWaveClientType = {
|
|
11
|
+
init(options) {
|
|
12
|
+
return registerPushWave(options);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default PushWaveClient;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { fetchApi } from "./utils/fetch";
|
|
2
|
+
import { getExpoToken } from "./utils/expoToken";
|
|
3
|
+
import getApplicationAttestation from "./attestation/getApplicationAttestation";
|
|
4
|
+
import { Platform } from "react-native";
|
|
5
|
+
import { PWLogger } from "./utils/pwLogger";
|
|
6
|
+
import { isSecretKey } from "./utils/apiKeyCheck";
|
|
7
|
+
|
|
8
|
+
export interface RegisterPushWaveClient {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RegisterPushWaveResponse {
|
|
13
|
+
success: boolean;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RegisterPushWaveOptions {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
expoToken: string;
|
|
20
|
+
platform: string;
|
|
21
|
+
appAttestation?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default async function registerPushWave(
|
|
25
|
+
{ apiKey }: RegisterPushWaveClient
|
|
26
|
+
): Promise<RegisterPushWaveResponse> {
|
|
27
|
+
|
|
28
|
+
if (isSecretKey(apiKey)) {
|
|
29
|
+
const warn = `\x1b[0m You are using your SECRET API key in a client environment. This key must NEVER be embedded in a mobile app.`;
|
|
30
|
+
PWLogger.warn(warn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const expoToken = await getExpoToken();
|
|
34
|
+
|
|
35
|
+
if (!expoToken) {
|
|
36
|
+
|
|
37
|
+
const message = "could not get ExpoToken";
|
|
38
|
+
|
|
39
|
+
PWLogger.error(message)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
message: "[PushWaveClient] Error: " + message
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const appAttestation = await getApplicationAttestation(apiKey);
|
|
48
|
+
|
|
49
|
+
if (!appAttestation) {
|
|
50
|
+
|
|
51
|
+
const message = `could not get ${Platform.OS} attestation.`;
|
|
52
|
+
|
|
53
|
+
PWLogger.error(message);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
message: `[PushWaveClient] Error: ` + message
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const path = "/v1/expo-tokens"
|
|
62
|
+
|
|
63
|
+
const options: RegisterPushWaveOptions = {
|
|
64
|
+
apiKey: apiKey,
|
|
65
|
+
expoToken: expoToken,
|
|
66
|
+
platform: Platform.OS,
|
|
67
|
+
appAttestation: appAttestation
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res: RegisterPushWaveResponse = await fetchApi(path, options)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
message: res.message,
|
|
76
|
+
};
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const e = err as Error;
|
|
79
|
+
|
|
80
|
+
PWLogger.error(e.message);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
message: e.message,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Notifications from "expo-notifications";
|
|
2
|
+
import Constants from "expo-constants";
|
|
3
|
+
|
|
4
|
+
export async function getExpoToken() {
|
|
5
|
+
try {
|
|
6
|
+
const existing = await Notifications.getPermissionsAsync();
|
|
7
|
+
const finalStatus =
|
|
8
|
+
existing.status === "granted"
|
|
9
|
+
? existing.status
|
|
10
|
+
: (await Notifications.requestPermissionsAsync()).status;
|
|
11
|
+
if (finalStatus !== "granted") return;
|
|
12
|
+
|
|
13
|
+
const projectId =
|
|
14
|
+
Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId;
|
|
15
|
+
|
|
16
|
+
const token = (
|
|
17
|
+
projectId
|
|
18
|
+
? await Notifications.getExpoPushTokenAsync({ projectId })
|
|
19
|
+
: await Notifications.getExpoPushTokenAsync()
|
|
20
|
+
).data;
|
|
21
|
+
|
|
22
|
+
return token;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new Error(`[expo-notifications] Error: ` + e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const BASE_URL = "https://pushwave.luruk-hai.fr";
|
|
2
|
+
|
|
3
|
+
export async function fetchApi<TResponse>(
|
|
4
|
+
path: string,
|
|
5
|
+
data: Record<string, any>
|
|
6
|
+
): Promise<TResponse> {
|
|
7
|
+
|
|
8
|
+
const url = BASE_URL + path;
|
|
9
|
+
|
|
10
|
+
const res = await fetch(url, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify(data),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const text = await res.text();
|
|
19
|
+
let json: any = null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
json = text ? JSON.parse(text) : null;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// JSON empty/invalid
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const apiError = (json?.error ?? json?.message ?? "") as string;
|
|
29
|
+
|
|
30
|
+
const message = `(${res.status}) ${apiError}`.trim();
|
|
31
|
+
|
|
32
|
+
throw new Error(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return json as TResponse;
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// utils/pwLogger.ts
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
bold: "\x1b[1m",
|
|
6
|
+
dim: "\x1b[2m",
|
|
7
|
+
red: "\x1b[31m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
green: "\x1b[32m"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const PWLogger = {
|
|
14
|
+
info: (...args: any[]) => {
|
|
15
|
+
if (__DEV__) {
|
|
16
|
+
console.log(
|
|
17
|
+
`${COLORS.cyan}${COLORS.bold}[PushWave INFO]${COLORS.reset}`,
|
|
18
|
+
...args
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
warn: (...args: any[]) => {
|
|
24
|
+
if (__DEV__) {
|
|
25
|
+
console.warn(
|
|
26
|
+
`${COLORS.yellow}${COLORS.bold}⚠️ [PushWave WARNING]${COLORS.reset}`,
|
|
27
|
+
...args
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
error: (...args: any[]) => {
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
console.error(
|
|
35
|
+
`${COLORS.red}${COLORS.bold}❌ [PushWave ERROR]${COLORS.reset}`,
|
|
36
|
+
...args
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
success: (...args: any[]) => {
|
|
42
|
+
if (__DEV__) {
|
|
43
|
+
console.log(
|
|
44
|
+
`${COLORS.green}${COLORS.bold}✔️ [PushWave]${COLORS.reset}`,
|
|
45
|
+
...args
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
File without changes
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"jsx": "react-native"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|