fix: route APNs delivery per-token by registered environment
Issue: after defaulting APNs delivery to production (#2381), development builds installed from Xcode stopped receiving notifications entirely: their sandbox device tokens were sent to the production APNs endpoint, rejected as BadDeviceToken, and dropped as dead. Fix: the iOS shell reads the aps-environment entitlement from the embedded provisioning profile and exposes it to the web layer as a document-start user script (added in capacitorDidLoad, since Capacitor replaces the userContentController after webViewConfiguration(for:)). Token registration reports the environment to the server, which stores it per token and groups delivery by environment for both relay and direct APNs sends. OPENCHAMBER_APNS_ENVIRONMENT remains as an explicit override forcing every send to one environment. TestFlight/App Store builds and older clients without the field default to production, preserving released behavior; the relay already accepts env per send request.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import UserNotifications
|
||||
import WebKit
|
||||
import WidgetKit
|
||||
|
||||
@UIApplicationMain
|
||||
@@ -60,6 +61,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
}
|
||||
|
||||
/// APNs environment of this build: "development" for Xcode/dev-signed installs,
|
||||
/// "production" for TestFlight/App Store. Read from the embedded provisioning profile's
|
||||
/// aps-environment entitlement; App Store builds carry no embedded profile and are
|
||||
/// production. Exposed to the web layer so the server can deliver each device token to
|
||||
/// the APNs endpoint that actually knows it (sandbox vs production).
|
||||
let apnsEnvironment: String = {
|
||||
guard let path = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"),
|
||||
let data = FileManager.default.contents(atPath: path),
|
||||
// isoLatin1, not ascii/utf8: the profile is a binary CMS envelope around the XML
|
||||
// plist, and only Latin-1 decodes arbitrary bytes without returning nil.
|
||||
let profile = String(data: data, encoding: .isoLatin1) else {
|
||||
return "production"
|
||||
}
|
||||
let pattern = "<key>aps-environment</key>\\s*<string>development</string>"
|
||||
return profile.range(of: pattern, options: .regularExpression) != nil ? "development" : "production"
|
||||
}()
|
||||
|
||||
/// Bridge subclass (referenced from Main.storyboard) whose only job is to expose the APNs
|
||||
/// environment as a document-start user script. This runs before any page JS, so token
|
||||
/// registration (useNativePushRegistration) always sees it — injecting later from the scene
|
||||
/// lifecycle raced the registration call and lost on first launch.
|
||||
///
|
||||
/// The script must be added in capacitorDidLoad(), NOT webViewConfiguration(for:): Capacitor's
|
||||
/// prepareWebView replaces the configuration's userContentController with its own right after
|
||||
/// calling webViewConfiguration(for:), which silently discards any user script added there.
|
||||
/// capacitorDidLoad() runs after that swap but before loadWebView() starts the initial page load.
|
||||
class BridgeViewController: CAPBridgeViewController {
|
||||
override func capacitorDidLoad() {
|
||||
super.capacitorDidLoad()
|
||||
let source = "window.__OPENCHAMBER_APNS_ENV__ = '\(apnsEnvironment)';"
|
||||
webView?.configuration.userContentController.addUserScript(
|
||||
WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// iOS 26 (TN3187) requires apps built with the latest SDK to adopt the UIScene
|
||||
// lifecycle. Capacitor 7's template still uses the legacy window setup, so we host a
|
||||
// minimal scene delegate here that loads the Main storyboard (CAPBridgeViewController)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!--Bridge View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
|
||||
<viewController id="BYZ-38-t0r" customClass="BridgeViewController" customModule="App" customModuleProvider="target" sceneMemberID="viewController"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
|
||||
Reference in New Issue
Block a user