Skip to content

Commit 0ee2f6e

Browse files
authored
feat: add notification trigger service (#904)
1 parent 1286539 commit 0ee2f6e

16 files changed

Lines changed: 866 additions & 16 deletions

File tree

infrastructure/control-panel/config/admin-enames.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"@7218b67d-da21-54d6-9a85-7c4db1d09768",
44
"@82f7a77a-f03a-52aa-88fc-1b1e488ad498",
55
"@35a31f0d-dd76-5780-b383-29f219fcae99",
6-
"@82f7a77a-f03a-52aa-88fc-1b1e488ad498"
6+
"@82f7a77a-f03a-52aa-88fc-1b1e488ad498",
7+
"@af7e4f55-ad9d-537c-81ef-4f3a234bdd2c"
78
]
89
}

infrastructure/control-panel/src/routes/notifications/+page.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@
326326
<ButtonAction
327327
variant="solid"
328328
size="sm"
329+
class="w-fit"
329330
isLoading={enameSending}
330331
blockingClick
331332
callback={sendByEName}
@@ -343,7 +344,9 @@
343344
</p>
344345
{#if deviceCount !== null}
345346
<p class="text-black-500 mt-1 text-sm font-medium">
346-
{deviceCount} device{deviceCount === 1 ? '' : 's'} with push token{deviceCount === 1 ? '' : 's'}
347+
{deviceCount} device{deviceCount === 1 ? '' : 's'} with push token{deviceCount === 1
348+
? ''
349+
: 's'}
347350
</p>
348351
{/if}
349352
<div class="mt-4 space-y-4">
@@ -383,6 +386,7 @@
383386
<ButtonAction
384387
variant="solid"
385388
size="sm"
389+
class="w-fit"
386390
isLoading={bulkSending}
387391
blockingClick
388392
callback={sendBulkAll}

infrastructure/eid-wallet/src-tauri/gen/android/app/build.gradle.kts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,22 @@ android {
2727
}
2828

2929
signingConfigs {
30-
create("release") {
31-
val keystorePropertiesFile = rootProject.file("keystore.properties")
32-
val keystoreProperties = Properties()
33-
if (keystorePropertiesFile.exists()) {
34-
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
35-
}
30+
val keystorePropertiesFile = rootProject.file("keystore.properties")
31+
val keystoreProperties = Properties()
32+
if (keystorePropertiesFile.exists()) {
33+
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
34+
}
35+
val hasKeystore = keystoreProperties["keyAlias"] != null &&
36+
keystoreProperties["password"] != null &&
37+
keystoreProperties["storeFile"] != null
3638

37-
keyAlias = keystoreProperties["keyAlias"] as String
38-
keyPassword = keystoreProperties["password"] as String
39-
storeFile = file(keystoreProperties["storeFile"] as String)
40-
storePassword = keystoreProperties["password"] as String
39+
if (hasKeystore) {
40+
create("release") {
41+
keyAlias = keystoreProperties["keyAlias"] as String
42+
keyPassword = keystoreProperties["password"] as String
43+
storeFile = file(keystoreProperties["storeFile"] as String)
44+
storePassword = keystoreProperties["password"] as String
45+
}
4146
}
4247
}
4348

@@ -54,7 +59,9 @@ android {
5459
}
5560
}
5661
getByName("release") {
57-
signingConfig = signingConfigs.getByName("release")
62+
if (signingConfigs.findByName("release") != null) {
63+
signingConfig = signingConfigs.getByName("release")
64+
}
5865
isMinifyEnabled = true
5966
proguardFiles(
6067
*fileTree(".") { include("**/*.pro") }

infrastructure/signature-validator/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,16 @@ export async function verifySignature(
330330
};
331331
}
332332

333+
console.log("Verifying signature for eName:", eName);
334+
console.log("Registry base URL:", registryBaseUrl);
335+
console.log("Signature:", signature);
336+
console.log("Payload:", payload);
337+
333338
// Get key binding certificates from eVault
334339
const keyBindingCertificates = await getKeyBindingCertificates(eName, registryBaseUrl);
335340

341+
console.log("Key binding certificates:", keyBindingCertificates);
342+
336343
if (keyBindingCertificates.length === 0) {
337344
return {
338345
valid: true,

notification-trigger/.env.example

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Notification Trigger
2+
# When run from the prototype monorepo, env vars are loaded from the repo root .env
3+
# (which has GOOGLE_APPLICATION_CREDENTIALS, APNS_*, etc.). Copy this file to .env
4+
# only if you need to override or run standalone.
5+
6+
# Server
7+
NOTIFICATION_TRIGGER_PORT=3998
8+
9+
# FCM (Android) - path to Firebase service account JSON
10+
# Download from Firebase Console → Project Settings → Service accounts
11+
GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/firebase-adminsdk-xxxxx.json"
12+
13+
# APNS (iOS) - from Apple Developer
14+
# Create .p8 key in Apple Developer → Keys
15+
APNS_KEY_PATH="/path/to/AuthKey_XXXXX.p8"
16+
APNS_KEY_ID="XXXXXXXXXX"
17+
APNS_TEAM_ID="XXXXXXXXXX"
18+
APNS_BUNDLE_ID="com.example.yourapp"
19+
20+
# Use production APNS (false for development/sandbox)
21+
APNS_PRODUCTION=false
22+
23+
# Optional: Live Activities broadcast channel ID (base64)
24+
# APNS_BROADCAST_CHANNEL_ID=

notification-trigger/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
.env

notification-trigger/README.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Notification Trigger
2+
3+
A standalone toy platform to send push notifications via **APNS** (iOS) and **FCM** (Android). Accepts a common payload structure and routes to the appropriate service based on platform.
4+
5+
Kept in this repo for testing purposes. Can be moved to its own project for standalone use.
6+
7+
## Quick start
8+
9+
```bash
10+
pnpm install
11+
cp .env.example .env
12+
# Edit .env with your credentials
13+
pnpm dev
14+
```
15+
16+
Open http://localhost:3998 to use the web UI.
17+
18+
## Requirements
19+
20+
- **Node.js** 18+
21+
- **FCM (Android)**: Firebase service account JSON
22+
- **APNS (iOS)**: Apple .p8 auth key + key ID, team ID, bundle ID
23+
24+
You can run with only one provider configured; the other will be disabled.
25+
26+
## API
27+
28+
### GET /api/health
29+
30+
Returns health status and which providers are configured.
31+
32+
```json
33+
{
34+
"ok": true,
35+
"apns": true,
36+
"fcm": true
37+
}
38+
```
39+
40+
### POST /api/send
41+
42+
Send a push notification.
43+
44+
**Request body:**
45+
46+
```json
47+
{
48+
"token": "<APNS or FCM device token>",
49+
"platform": "ios" | "android" | null,
50+
"payload": {
51+
"title": "Hello",
52+
"body": "Notification body",
53+
"subtitle": "Optional subtitle",
54+
"data": { "key": "value" },
55+
"sound": "default",
56+
"badge": 1,
57+
"clickAction": "category-id"
58+
}
59+
}
60+
```
61+
62+
- **token**: APNS device token (iOS) or FCM registration token (Android)
63+
- **platform**: Optional. If omitted, auto-detected from token format (64 hex chars → iOS/APNS, else → Android/FCM)
64+
- **payload**: Common structure; `title` and `body` are required. `data` values must be strings for FCM compatibility.
65+
66+
**Example (curl):**
67+
68+
```bash
69+
curl -X POST http://localhost:3998/api/send \
70+
-H "Content-Type: application/json" \
71+
-d '{
72+
"token": "YOUR_DEVICE_TOKEN",
73+
"payload": {
74+
"title": "Test",
75+
"body": "Hello from notification trigger"
76+
}
77+
}'
78+
```
79+
80+
## Environment
81+
82+
Copy `.env.example` to `.env` and fill in your credentials. See `.env.example` for all required variables.
83+
84+
| Variable | Required | Description |
85+
|----------|----------|--------------|
86+
| `NOTIFICATION_TRIGGER_PORT` | No | Server port (default: 3998) |
87+
| `GOOGLE_APPLICATION_CREDENTIALS` | For FCM | Path to Firebase service account JSON |
88+
| `APNS_KEY_PATH` | For APNS | Path to .p8 APNS auth key |
89+
| `APNS_KEY_ID` | For APNS | APNS key ID |
90+
| `APNS_TEAM_ID` | For APNS | Apple team ID |
91+
| `APNS_BUNDLE_ID` | For APNS | App bundle ID |
92+
| `APNS_PRODUCTION` | No | `true` for production APNS (default: `false`) |
93+
| `APNS_BROADCAST_CHANNEL_ID` | No | Base64 channel ID for Live Activities (optional) |
94+
95+
## Build & run
96+
97+
```bash
98+
pnpm build
99+
pnpm start
100+
```
101+
102+
## Integration with this repo
103+
104+
When used from the prototype monorepo:
105+
106+
- Control panel uses `NOTIFICATION_TRIGGER_URL` (e.g. `http://localhost:3998`) to send notifications
107+
- The provisioner (evault-core) stores device tokens; the control panel fetches them and sends via this service

notification-trigger/package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "notification-trigger",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "Standalone toy platform to trigger push notifications via APNS/FCM. Kept for testing.",
6+
"main": "dist/index.js",
7+
"scripts": {
8+
"dev": "tsx watch src/index.ts",
9+
"build": "tsc && cp -r public dist/",
10+
"start": "node dist/index.js"
11+
},
12+
"dependencies": {
13+
"apn": "^2.2.0",
14+
"cors": "^2.8.5",
15+
"dotenv": "^16.4.5",
16+
"express": "^4.18.2",
17+
"firebase-admin": "^12.0.0"
18+
},
19+
"devDependencies": {
20+
"@types/cors": "^2.8.18",
21+
"@types/express": "^4.17.21",
22+
"@types/node": "^20.11.24",
23+
"tsx": "^4.7.1",
24+
"typescript": "^5.3.3"
25+
}
26+
}

0 commit comments

Comments
 (0)