Skip to content
Open
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
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
79 changes: 1 addition & 78 deletions ios/src/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,89 +1,12 @@
PODS:
- Firebase/CoreOnly (12.12.1):
- FirebaseCore (~> 12.12.1)
- Firebase/Messaging (12.12.1):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.12.0)
- FirebaseCore (12.12.1):
- FirebaseCoreInternal (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.12.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.12.0):
- FirebaseCore (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.12.0):
- FirebaseCore (~> 12.12.0)
- FirebaseInstallations (~> 12.12.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- PromisesObjC (2.4.0)

DEPENDENCIES:
- Firebase/Messaging

SPEC REPOS:
trunk:
- Firebase
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC

SPEC CHECKSUMS:
Firebase: 14f11e91129d246a8a6166b4c1c2ea61b56806ec
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47

PODFILE CHECKSUM: 5cd187e394ea190fb18c0c57d79f58afdc0a7b86
PODFILE CHECKSUM: 613fd0f1fe9f534e781d74b06ed8e753449a3335

COCOAPODS: 1.16.2
4 changes: 0 additions & 4 deletions ios/src/ZotMeet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
objects = {

/* Begin PBXBuildFile section */
25D3517F267A48E0002E5DC0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */; };
595F23A525CEFBFE0053416C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 595F239A25CEFBFD0053416C /* Settings.swift */; };
595F23A625CEFBFE0053416C /* Entitlements in Resources */ = {isa = PBXBuildFile; fileRef = 595F239B25CEFBFD0053416C /* Entitlements */; };
595F23A725CEFBFE0053416C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 595F239C25CEFBFD0053416C /* Assets.xcassets */; };
Expand All @@ -23,7 +22,6 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "ZotMeet/GoogleService-Info.plist"; sourceTree = "<group>"; };
4C1C2162A8048FDF52B2A9D0 /* Pods-ZotMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ZotMeet.release.xcconfig"; path = "Target Support Files/Pods-ZotMeet/Pods-ZotMeet.release.xcconfig"; sourceTree = "<group>"; };
59333BAA25CFF706003392A4 /* ZotMeet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ZotMeet.app; sourceTree = BUILT_PRODUCTS_DIR; };
595F239A25CEFBFD0053416C /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Settings.swift; path = ZotMeet/Settings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -78,7 +76,6 @@
595F239C25CEFBFD0053416C /* Assets.xcassets */,
595F239B25CEFBFD0053416C /* Entitlements */,
595F23A025CEFBFE0053416C /* Info.plist */,
25D3517E267A48E0002E5DC0 /* GoogleService-Info.plist */,
595F23A325CEFBFE0053416C /* Printer.swift */,
595F239F25CEFBFE0053416C /* PushNotifications.swift */,
595F239D25CEFBFD0053416C /* SceneDelegate.swift */,
Expand Down Expand Up @@ -158,7 +155,6 @@
CDC0FE292388222C002C8D56 /* Main.storyboard in Resources */,
CDC0FE2A2388222C002C8D56 /* LaunchScreen.storyboard in Resources */,
595F23A725CEFBFE0053416C /* Assets.xcassets in Resources */,
25D3517F267A48E0002E5DC0 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
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]
}
34 changes: 0 additions & 34 deletions ios/src/ZotMeet/GoogleService-Info.plist

This file was deleted.

Loading
Loading