Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/ios/capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const config: CapacitorConfig = {
iOSClientId: '636969838408-0jbv7gq9793m0uchdrjlr8v1k6m5lh59.apps.googleusercontent.com',
},
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
};

Expand Down
2 changes: 2 additions & 0 deletions apps/ios/ios/App/App/App.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
<array>
<string>Default</string>
</array>
<key>aps-environment</key>
<string>production</string>
</dict>
</plist>
10 changes: 10 additions & 0 deletions apps/ios/ios/App/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}

// MARK: - Push Notifications

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
}

func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
Expand Down
1 change: 1 addition & 0 deletions apps/ios/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@capacitor/ios": "^7.0.3",
"@capacitor/keyboard": "^7.0.3",
"@capacitor/preferences": "^7.0.3",
"@capacitor/push-notifications": "^7.0.0",
"@capacitor/splash-screen": "^7.0.3",
"@capacitor/status-bar": "^7.0.3",
"@capgo/capacitor-social-login": "^7.20.0"
Expand Down
106 changes: 106 additions & 0 deletions apps/web/src/app/api/notifications/push-tokens/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { NextResponse } from 'next/server';
import {
registerPushToken,
unregisterPushToken,
getUserPushTokens,
} from '@pagespace/lib/notifications';
import { loggers } from '@pagespace/lib/server';
import { authenticateRequestWithOptions, isAuthError } from '@/lib/auth';

const AUTH_OPTIONS = { allow: ['session'] as const, requireCSRF: true };

export async function GET(req: Request) {
const auth = await authenticateRequestWithOptions(req, AUTH_OPTIONS);
if (isAuthError(auth)) {
return auth.error;
}
const userId = auth.userId;

try {
const tokens = await getUserPushTokens(userId);
return NextResponse.json({ tokens });
} catch (error) {
loggers.api.error('Error fetching push tokens:', error as Error);
return NextResponse.json(
{ error: 'Failed to fetch push tokens' },
{ status: 500 }
);
}
}

export async function POST(req: Request) {
const auth = await authenticateRequestWithOptions(req, AUTH_OPTIONS);
if (isAuthError(auth)) {
return auth.error;
}
const userId = auth.userId;

try {
const body = await req.json();
const { token, platform, deviceId, deviceName, webPushSubscription } = body;

if (!token || !platform) {
return NextResponse.json(
{ error: 'Token and platform are required' },
{ status: 400 }
);
}

if (!['ios', 'android', 'web'].includes(platform)) {
return NextResponse.json(
{ error: 'Invalid platform. Must be ios, android, or web' },
{ status: 400 }
);
}

const result = await registerPushToken(
userId,
token,
platform,
deviceId,
deviceName,
webPushSubscription
);

return NextResponse.json({
success: true,
tokenId: result.id,
});
} catch (error) {
loggers.api.error('Error registering push token:', error as Error);
return NextResponse.json(
{ error: 'Failed to register push token' },
{ status: 500 }
);
}
}

export async function DELETE(req: Request) {
const auth = await authenticateRequestWithOptions(req, AUTH_OPTIONS);
if (isAuthError(auth)) {
return auth.error;
}
const userId = auth.userId;

try {
const body = await req.json();
const { token } = body;

if (!token) {
return NextResponse.json(
{ error: 'Token is required' },
{ status: 400 }
);
}

await unregisterPushToken(userId, token);

return NextResponse.json({ success: true });
} catch (error) {
loggers.api.error('Error unregistering push token:', error as Error);
return NextResponse.json(
{ error: 'Failed to unregister push token' },
{ status: 500 }
);
}
}
Loading