Add Apple push registration alongside message refresh for TestFlight
This commit is contained in:
parent
75f8850115
commit
aab402f612
|
|
@ -292,12 +292,14 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CODE_SIGN_ENTITLEMENTS = Kasztelan/Kasztelan.entitlements;
|
||||
APS_ENVIRONMENT = development;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Kasztelan/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
MARKETING_VERSION = 1.0.9;
|
||||
MARKETING_VERSION = 1.0.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -316,12 +318,14 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CODE_SIGN_ENTITLEMENTS = Kasztelan/Kasztelan.entitlements;
|
||||
APS_ENVIRONMENT = production;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = Kasztelan/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
MARKETING_VERSION = 1.0.9;
|
||||
MARKETING_VERSION = 1.0.10;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = uk.co.kasztelan.app.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ final class APIClient {
|
|||
try await request("GET", path: "/notifications/latest", token: token)
|
||||
}
|
||||
|
||||
func registerPush(deviceToken: String, environment: String, token: String) async throws {
|
||||
let _: APIEnvelope = try await request("POST", path: "/push/device", token: token, body: [
|
||||
"device_token": deviceToken, "environment": environment
|
||||
])
|
||||
}
|
||||
|
||||
func unregisterPush(deviceToken: String, token: String) async throws {
|
||||
let _: APIEnvelope = try await request("DELETE", path: "/push/device", token: token, body: ["device_token": deviceToken])
|
||||
}
|
||||
|
||||
func wallet(token: String) async throws -> APIEnvelope {
|
||||
try await request("GET", path: "/wallet", token: token)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ final class AppModel: ObservableObject {
|
|||
let wallet = DiscountCardStore()
|
||||
await wallet.sync(using: self)
|
||||
}
|
||||
await NotificationService.unregisterPush(session: currentToken)
|
||||
clearSession()
|
||||
try? await api.logout(token: currentToken)
|
||||
}
|
||||
|
|
@ -175,6 +176,7 @@ final class AppModel: ObservableObject {
|
|||
|
||||
func refresh() async {
|
||||
guard isAuthenticated else { return }
|
||||
await NotificationService.syncPushRegistration()
|
||||
do {
|
||||
if let response = try await fetchAccount() { apply(response) }
|
||||
} catch {
|
||||
|
|
@ -271,6 +273,7 @@ final class AppModel: ObservableObject {
|
|||
guard let token = response.token, !token.isEmpty else { throw APIError.invalidResponse }
|
||||
try KeychainStore.save(token: token)
|
||||
self.token = token
|
||||
Task { await NotificationService.requestPermission() }
|
||||
apply(response)
|
||||
beginLiveRefresh()
|
||||
Task { await refreshMessages(showNotifications: true) }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>KASZTELANPushEnvironment</key>
|
||||
<string>$(APS_ENVIRONMENT)</string>
|
||||
<key>KASZTELANAppKey</key>
|
||||
<string>$(KASZTELAN_APP_KEY)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
|||
3
Kasztelan/Kasztelan.entitlements
Normal file
3
Kasztelan/Kasztelan.entitlements
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict><key>aps-environment</key><string>$(APS_ENVIRONMENT)</string></dict></plist>
|
||||
|
|
@ -37,6 +37,14 @@ final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCent
|
|||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
Task { await NotificationService.receivedDeviceToken(deviceToken) }
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
// APNs registration is attempted again on the next activation.
|
||||
}
|
||||
|
||||
private func indexInSpotlight() {
|
||||
let attributes = CSSearchableItemAttributeSet(contentType: .application)
|
||||
attributes.title = "Kasztelan"
|
||||
|
|
@ -149,7 +157,10 @@ struct KasztelanApp: App {
|
|||
}
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
if phase == .active { model.beginLiveRefresh() }
|
||||
if phase == .active {
|
||||
model.beginLiveRefresh()
|
||||
Task { await NotificationService.requestPermission() }
|
||||
}
|
||||
else { model.stopLiveRefresh() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,56 @@
|
|||
import Foundation
|
||||
import UserNotifications
|
||||
import UIKit
|
||||
|
||||
enum NotificationService {
|
||||
@MainActor
|
||||
static func requestPermission() async {
|
||||
_ = try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
@MainActor private static var deviceToken: String?
|
||||
@MainActor private static var registeredSession: String?
|
||||
|
||||
@MainActor
|
||||
static func receivedDeviceToken(_ data: Data) async {
|
||||
deviceToken = data.map { String(format: "%02x", $0) }.joined()
|
||||
registeredSession = nil
|
||||
await syncPushRegistration()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func syncPushRegistration() async {
|
||||
let session = KeychainStore.load()
|
||||
guard !session.isEmpty, let deviceToken, registeredSession != session else { return }
|
||||
let environment = Bundle.main.object(forInfoDictionaryKey: "KASZTELANPushEnvironment") as? String ?? "production"
|
||||
do {
|
||||
try await APIClient().registerPush(deviceToken: deviceToken, environment: environment, token: session)
|
||||
// The account may have changed while the request was in flight.
|
||||
if KeychainStore.load() == session { registeredSession = session }
|
||||
} catch {
|
||||
// Retry when the app next refreshes; keep account access working offline.
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func unregisterPush(session: String) async {
|
||||
registeredSession = nil
|
||||
guard let deviceToken else { return }
|
||||
try? await APIClient().unregisterPush(deviceToken: deviceToken, token: session)
|
||||
}
|
||||
|
||||
static func show(_ message: KasztelanMessage) async {
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else { return }
|
||||
let delivered = await UNUserNotificationCenter.current().deliveredNotifications()
|
||||
guard !delivered.contains(where: {
|
||||
($0.request.content.userInfo["message_id"] as? Int) == message.id
|
||||
}) else { return }
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "KASZTELAN"
|
||||
content.subtitle = message.title
|
||||
content.body = message.text
|
||||
content.sound = .default
|
||||
content.threadIdentifier = "kasztelan-messages"
|
||||
content.categoryIdentifier = "KASZTELAN_MESSAGE"
|
||||
|
|
|
|||
|
|
@ -41,5 +41,5 @@ workflows:
|
|||
publishing:
|
||||
app_store_connect:
|
||||
auth: integration
|
||||
submit_to_testflight: false
|
||||
submit_to_testflight: true
|
||||
submit_to_app_store: false
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user