Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ NEXT_PUBLIC_IOS_BUNDLE_ID=com.zotmeet.app
# Register the service worker in non-production builds (e.g. when testing with PWABuilder
# against a preview URL). Leave unset in local dev unless you need SW debugging.
# NEXT_PUBLIC_ENABLE_SW=true

# iOS push via AWS SNS (APNs platform application in AWS Console / IaC).
# Create an Apple platform application, upload your APNs key (.p8), then set:
# SNS_IOS_PLATFORM_APPLICATION_ARN=arn:aws:sns:us-west-1:ACCOUNT:app/APNS/ZotMeet
# SNS_REGION=us-west-1
# Use SNS_IOS_APNS_ENV=sandbox for Xcode debug builds; production for TestFlight/App Store.
# SNS_IOS_APNS_ENV=production
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,27 @@ Graph, robots, sitemap, and invite/email links.
4. Click `Package for stores` → `iOS` → `Generate Package`.
5. Take note of the `Bundle ID` and download the package.
6. Open the generated `.xcworkspace` in Xcode (≥ iOS 17 SDK), run
`pod install` in `src/`, build, and archive for distribution following the
`pod install` in `ios/src/`, build, and archive for distribution following the
[PWA Builder iOS guide](https://docs.pwabuilder.com/#/builder/app-store).

### iOS push notifications (AWS SNS)

Native push uses **AWS SNS → APNs** (same AWS account pattern as SES email).

1. In [Apple Developer](https://developer.apple.com/account/resources/authkeys/list),
create an APNs key (`.p8`) and note Key ID, Team ID, and your app Bundle ID.
2. In AWS Console → SNS → Mobile → Push notifications → Platform applications,
create an **Apple iOS/VoIP/Mac** platform application (token-based `.p8` auth).
3. Set environment variables (see `.env.example`):
- `SNS_IOS_PLATFORM_APPLICATION_ARN` — platform application ARN
- `SNS_REGION` — region where the platform app was created (default `us-west-1` in `sst.config.ts`)
- `SNS_IOS_APNS_ENV` — `sandbox` for Xcode debug builds, `production` for TestFlight/App Store
4. Deploy so the Next.js workload has SNS IAM permissions (`sst.config.ts` includes
`sns:Publish`, `CreatePlatformEndpoint`, etc.).

After login in the native app, iOS prompts for notification permission, registers an
APNs device token with the server, and the server stores an SNS platform endpoint ARN.

### Regenerating PWA assets

```bash
Expand Down
5 changes: 1 addition & 4 deletions ios/src/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ target 'ZotMeet' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!

# Add the pod for Firebase Cloud Messaging
pod 'Firebase/Messaging'

end

post_install do |installer|
Expand All @@ -16,4 +13,4 @@ post_install do |installer|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.4'
end
end
end
end
109 changes: 14 additions & 95 deletions ios/src/ZotMeet/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,143 +1,62 @@
import UIKit
import FirebaseCore
import FirebaseMessaging


@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window : UIWindow?

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

// TODO: if we're using Firebase, uncomment next string
//FirebaseApp.configure()

// [START set_messaging_delegate]
Messaging.messaging().delegate = self
// [END set_messaging_delegate]
// Register for remote notifications. This shows a permission dialog on first run, to
// show the dialog at a more appropriate time move this registration accordingly.
// [START register_for_notifications]

UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .authorized, .ephemeral, .provisional:
registerForPushNotifications()
default:
return
}
}

// let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
// UNUserNotificationCenter.current().requestAuthorization(
// options: authOptions,
// completionHandler: {_, _ in })

// TODO: if we're using Firebase, uncomment next string
// application.registerForRemoteNotifications()

// [END register_for_notifications]
return true
}

// [START receive_message]
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 1: \(messageID)")
}

// Print full message.
print("push userInfo 1:", userInfo)
sendPushToWebView(userInfo: userInfo)
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// If you are receiving a notification message while your app is in the background,
// this callback will not be fired till the user taps on the notification launching the application.
// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 2: \(messageID)")
}

// Print full message. **
print("push userInfo 2:", userInfo)
sendPushToWebView(userInfo: userInfo)

completionHandler(UIBackgroundFetchResult.newData)
}

// [END receive_message]
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Unable to register for remote notifications: \(error.localizedDescription)")
}

// This function is added here only for debugging purposes, and can be removed if swizzling is enabled.
// If swizzling is disabled then this function must be implemented so that the APNs token can be paired to
// the FCM registration token.
// func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// print("APNs token retrieved: \(deviceToken)")
//
// // With swizzling disabled you must set the APNs token here.
// // Messaging.messaging().apnsToken = deviceToken
// }
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Use the existing apnsTokenHexString(from:) helper instead of duplicating APNs token formatting logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ios/src/ZotMeet/AppDelegate.swift, line 39:

<comment>Use the existing `apnsTokenHexString(from:)` helper instead of duplicating APNs token formatting logic.</comment>

<file context>
@@ -30,127 +18,45 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
-//        // Messaging.messaging().apnsToken = deviceToken
-//      }
+      func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
+        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
+        setApnsDeviceTokenHex(token)
+        sendApnsTokenToWebView(token: token)
</file context>
Suggested change
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
let token = apnsTokenHexString(from: deviceToken)

setApnsDeviceTokenHex(token)
sendApnsTokenToWebView(token: token)
}
}

// [START ios_10_message_handling]
extension AppDelegate : UNUserNotificationCenterDelegate {

func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo

// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID: 3 \(messageID)")
}

// Print full message.
print("push userInfo 3:", userInfo)
sendPushToWebView(userInfo: userInfo)

// Change this to your preferred presentation option
completionHandler([[.banner, .list, .sound]])
}

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Print message ID.
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID 4: \(messageID)")
}

// With swizzling disabled you must let Messaging know about the message, for Analytics
// Messaging.messaging().appDidReceiveMessage(userInfo)
// Print full message.
print("push userInfo 4:", userInfo)
sendPushClickToWebView(userInfo: userInfo)

completionHandler()
}
}
// [END ios_10_message_handling]

extension AppDelegate : MessagingDelegate {
// [START refresh_token]
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")

let dataDict:[String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
handleFCMToken()
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
// [END refresh_token]
}
Loading
Loading