Skip to content

Commit e5ce75c

Browse files
Merge pull request #309 from CropWatchDevelopment/develop
Develop
2 parents e2e72f3 + d465f05 commit e5ce75c

6 files changed

Lines changed: 258 additions & 0 deletions

File tree

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# OneSignal configuration (SvelteKit style)
2+
# Public values (exposed to client build):
3+
PUBLIC_ONESIGNAL_APP_ID=00000000-0000-0000-0000-000000000000
4+
PUBLIC_ONESIGNAL_SAFARI_WEB_ID=web.onesignal.auto.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
5+
# Private (server only):
6+
PRIVATE_ONESIGNAL_REST_API_KEY=OS_REST_API_KEY_HERE
7+
8+
# Existing variables (append your current ones below if you have a separate example file)
9+
PUBLIC_SITE_URL=https://cropwatch.io

src/lib/onesignalPublic.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { env as publicEnv } from '$env/dynamic/public';
2+
3+
// Public-only config for client web push setup.
4+
export const ONE_SIGNAL_PUBLIC_CONFIG = {
5+
appId: publicEnv.PUBLIC_ONESIGNAL_APP_ID,
6+
safari_web_id: publicEnv.PUBLIC_ONESIGNAL_SAFARI_WEB_ID
7+
};

src/lib/server/OneSignalService.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService';
2+
import { env as publicEnv } from '$env/dynamic/public';
3+
import { env as privateEnv } from '$env/dynamic/private';
4+
5+
/**
6+
* Server-side OneSignal push helper (2025 docs: Create message API)
7+
* Uses SvelteKit dynamic env so build won't fail if vars absent at build-time.
8+
* Required at runtime:
9+
* PUBLIC_ONESIGNAL_APP_ID
10+
* PRIVATE_ONESIGNAL_REST_API_KEY
11+
*/
12+
export class OneSignalService {
13+
constructor(private errorHandler: ErrorHandlingService) {}
14+
15+
private assertEnv() {
16+
if (!publicEnv.PUBLIC_ONESIGNAL_APP_ID)
17+
throw new Error('PUBLIC_ONESIGNAL_APP_ID not configured');
18+
if (!privateEnv.PRIVATE_ONESIGNAL_REST_API_KEY)
19+
throw new Error('PRIVATE_ONESIGNAL_REST_API_KEY not configured');
20+
}
21+
22+
private baseHeaders() {
23+
this.assertEnv();
24+
return {
25+
'content-type': 'application/json; charset=utf-8',
26+
authorization: `Key ${privateEnv.PRIVATE_ONESIGNAL_REST_API_KEY}`
27+
};
28+
}
29+
30+
async sendPush(options: {
31+
headings?: Record<string, string>;
32+
contents: Record<string, string>;
33+
externalUserIds?: string[];
34+
includedSegments?: string[];
35+
data?: Record<string, unknown>;
36+
iosAttachments?: Record<string, string>;
37+
bigPicture?: string;
38+
name?: string;
39+
}) {
40+
try {
41+
this.assertEnv();
42+
const {
43+
headings,
44+
contents,
45+
externalUserIds,
46+
includedSegments,
47+
data,
48+
iosAttachments,
49+
bigPicture,
50+
name
51+
} = options;
52+
if (!contents || Object.keys(contents).length === 0) throw new Error('contents is required');
53+
if (!externalUserIds?.length && !includedSegments?.length)
54+
throw new Error('Must supply externalUserIds or includedSegments');
55+
56+
const body: any = {
57+
app_id: publicEnv.PUBLIC_ONESIGNAL_APP_ID,
58+
target_channel: 'push',
59+
contents,
60+
headings,
61+
data,
62+
name
63+
};
64+
if (externalUserIds?.length) body.include_external_user_ids = externalUserIds;
65+
if (includedSegments?.length) body.included_segments = includedSegments;
66+
if (iosAttachments) body.ios_attachments = iosAttachments;
67+
if (bigPicture) body.big_picture = bigPicture;
68+
69+
const res = await fetch('https://api.onesignal.com/notifications', {
70+
method: 'POST',
71+
headers: this.baseHeaders(),
72+
body: JSON.stringify(body)
73+
});
74+
if (!res.ok) {
75+
const text = await res.text();
76+
throw new Error(`OneSignal error ${res.status}: ${text}`);
77+
}
78+
return await res.json();
79+
} catch (err) {
80+
this.errorHandler.logError(err as Error);
81+
throw err;
82+
}
83+
}
84+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { ErrorHandlingService } from '../errors/ErrorHandlingService';
2+
// SvelteKit env imports. Public App ID may be used on client; REST key must stay private.
3+
import { PUBLIC_ONESIGNAL_APP_ID, PUBLIC_ONESIGNAL_SAFARI_WEB_ID } from '$env/static/public';
4+
import { PRIVATE_ONESIGNAL_REST_API_KEY } from '$env/static/private';
5+
6+
/**
7+
* OneSignal push helper (2025 docs: Create message API)
8+
* Env vars (SvelteKit style):
9+
* PUBLIC_ONESIGNAL_APP_ID (public)
10+
* PUBLIC_ONESIGNAL_SAFARI_WEB_ID (public, optional for Safari web push)
11+
* PRIVATE_ONESIGNAL_REST_API_KEY (server only)
12+
*/
13+
export class OneSignalService {
14+
constructor(private errorHandler: ErrorHandlingService) {}
15+
16+
private assertEnv() {
17+
if (!PUBLIC_ONESIGNAL_APP_ID) throw new Error('PUBLIC_ONESIGNAL_APP_ID not configured');
18+
if (!PRIVATE_ONESIGNAL_REST_API_KEY)
19+
throw new Error('PRIVATE_ONESIGNAL_REST_API_KEY not configured');
20+
}
21+
22+
private baseHeaders() {
23+
this.assertEnv();
24+
return {
25+
'content-type': 'application/json; charset=utf-8',
26+
authorization: `Key ${PRIVATE_ONESIGNAL_REST_API_KEY}`
27+
};
28+
}
29+
30+
/**
31+
* Send a push notification.
32+
* Provide either externalUserIds (preferred) or includedSegments.
33+
*/
34+
async sendPush(options: {
35+
headings?: Record<string, string>;
36+
contents: Record<string, string>;
37+
externalUserIds?: string[]; // OneSignal External IDs
38+
includedSegments?: string[]; // e.g. ['Test Users']
39+
data?: Record<string, unknown>;
40+
iosAttachments?: Record<string, string>;
41+
bigPicture?: string; // Android image
42+
name?: string; // internal name in dashboard
43+
}): Promise<any> {
44+
try {
45+
this.assertEnv();
46+
const {
47+
headings,
48+
contents,
49+
externalUserIds,
50+
includedSegments,
51+
data,
52+
iosAttachments,
53+
bigPicture,
54+
name
55+
} = options;
56+
57+
if (!contents || Object.keys(contents).length === 0) {
58+
throw new Error('contents is required');
59+
}
60+
if (!externalUserIds?.length && !includedSegments?.length) {
61+
throw new Error('Must supply externalUserIds or includedSegments');
62+
}
63+
64+
const body: any = {
65+
app_id: PUBLIC_ONESIGNAL_APP_ID,
66+
target_channel: 'push',
67+
contents,
68+
headings,
69+
data,
70+
name
71+
};
72+
if (externalUserIds?.length) body.include_external_user_ids = externalUserIds;
73+
if (includedSegments?.length) body.included_segments = includedSegments;
74+
if (iosAttachments) body.ios_attachments = iosAttachments;
75+
if (bigPicture) body.big_picture = bigPicture;
76+
77+
const res = await fetch('https://api.onesignal.com/notifications', {
78+
method: 'POST',
79+
headers: this.baseHeaders(),
80+
body: JSON.stringify(body)
81+
});
82+
if (!res.ok) {
83+
const text = await res.text();
84+
throw new Error(`OneSignal error ${res.status}: ${text}`);
85+
}
86+
return await res.json();
87+
} catch (err) {
88+
this.errorHandler.logError(err as Error);
89+
throw err;
90+
}
91+
}
92+
}
93+
94+
// Optional helper for web push initialization (Safari ID is exposed for completeness)
95+
export const ONE_SIGNAL_PUBLIC_CONFIG = {
96+
appId: PUBLIC_ONESIGNAL_APP_ID,
97+
safari_web_id: PUBLIC_ONESIGNAL_SAFARI_WEB_ID
98+
};

src/routes/+layout.svelte

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { onMount } from 'svelte';
1414
import '../app.css';
1515
import { info, warning } from '$lib/stores/toast.svelte';
16+
import { ONE_SIGNAL_PUBLIC_CONFIG } from '$lib/onesignalPublic';
1617
1718
// No preloading needed - dashboard will load its data when navigated to
1819
@@ -75,6 +76,29 @@
7576
i18n.initialize();
7677
});
7778
79+
// OneSignal Web Push (2025 docs style) - only loads if appId present
80+
onMount(() => {
81+
if (typeof window === 'undefined') return;
82+
if (!ONE_SIGNAL_PUBLIC_CONFIG.appId) return;
83+
// Inject script once
84+
if (!document.querySelector('script[data-onesignal-sdk]')) {
85+
const s = document.createElement('script');
86+
s.src = 'https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js';
87+
s.defer = true;
88+
s.setAttribute('data-onesignal-sdk', 'true');
89+
document.head.appendChild(s);
90+
}
91+
// Queue init
92+
(window as any).OneSignalDeferred = (window as any).OneSignalDeferred || [];
93+
(window as any).OneSignalDeferred.push(async function (OneSignal: any) {
94+
await OneSignal.init({
95+
appId: ONE_SIGNAL_PUBLIC_CONFIG.appId,
96+
safari_web_id: ONE_SIGNAL_PUBLIC_CONFIG.safari_web_id,
97+
notifyButton: { enable: true }
98+
});
99+
});
100+
});
101+
78102
// Handle navigation loading states with a small delay to avoid flash on fast transitions
79103
let navTimer: ReturnType<typeof setTimeout> | null = null;
80104
beforeNavigate(() => {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { RequestHandler } from '@sveltejs/kit';
2+
import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService';
3+
import { OneSignalService } from '$lib/server/OneSignalService';
4+
5+
/**
6+
* Test endpoint to send a push to either provided externalUserIds or the Test Users segment.
7+
* Secure this route (add auth / role checks) before using outside local dev.
8+
* Uses env vars: PUBLIC_ONESIGNAL_APP_ID / PRIVATE_ONESIGNAL_REST_API_KEY
9+
* POST body JSON: { contents: { en: "Hello" }, externalUserIds?: ["user123"], includedSegments?: ["Test Users"] }
10+
*/
11+
export const POST: RequestHandler = async ({ request }) => {
12+
const errorHandler = new ErrorHandlingService();
13+
const oneSignal = new OneSignalService(errorHandler);
14+
try {
15+
const body = await request.json();
16+
// Basic validation
17+
if (!body.contents) {
18+
return new Response(JSON.stringify({ error: 'contents required' }), { status: 400 });
19+
}
20+
21+
const result = await oneSignal.sendPush({
22+
contents: body.contents,
23+
headings: body.headings,
24+
externalUserIds: body.externalUserIds,
25+
includedSegments: body.includedSegments || (!body.externalUserIds && ['Test Users']),
26+
data: body.data,
27+
iosAttachments: body.iosAttachments,
28+
bigPicture: body.bigPicture,
29+
name: body.name || 'Test API Push'
30+
});
31+
32+
return new Response(JSON.stringify({ success: true, result }), { status: 200 });
33+
} catch (err) {
34+
return new Response(JSON.stringify({ error: (err as Error).message }), { status: 500 });
35+
}
36+
};

0 commit comments

Comments
 (0)