Ionic iOS上传TestFlight后Xcode调试Push Notification失效问题
问题现象
我已成功实现Ionic iOS端的Push Notification功能,但将应用上传发布至TestFlight后出现异常:TestFlight环境安装的应用可正常接收推送,此前运行正常的Xcode调试构建版本却无法接收推送。
基础配置与环境信息
我使用Cordova 9集成Firebase FCM实现推送能力,配置的Apple相关证书如下:
- Apple Sandbox Push Services
- Distribution Managed
- iOS Distribution
- Development
当前项目运行环境信息:
Ionic: ionic (Ionic CLI) : 4.12.0 Ionic Framework : @ionic/angular 5.2.3 @angular-devkit/build-angular : 0.901.11 @angular-devkit/schematics : 9.1.15 @angular/cli : 9.1.15 @ionic/angular-toolkit : 2.2.0 Cordova: cordova (Cordova CLI) : 8.1.2 (cordova-lib@8.1.1) Cordova Platforms : android 7.1.4, browser 5.0.4, ios 6.1.0 Cordova Plugins : cordova-plugin-ionic-keyboard 2.2.0, cordova-plugin-ionic-webview 5.0.0, (and 28 other plugins) System: ios-sim : 8.0.2 NodeJS : v10.14.1 (/usr/local/bin/node) npm : 7.21.1 OS : macOS Monterey Xcode : Xcode 13.4 Build version 13F17a
相关实现代码
服务端推送逻辑(PHP)
function send_notification ($token, $message, $title) { $path_to_firebase_cm = 'https://fcm.googleapis.com/fcm/send'; $fields = array( 'to' => $token, 'notification' => array('title' => $title, 'body' => $message, 'click_action' => 'FCM_PLUGIN_ACTIVITY'), 'data' => array('message' => $message, 'title' => $title), 'sound' => 'default' ); $headers = array( 'Authorization:key=AAAAf.............' ); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $path_to_firebase_cm); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 ); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields)); $result = curl_exec($ch); curl_close($ch); return $result; }
前端推送逻辑(app.component.ts)
{ this.backgroundMode.enable(); this.badge.set(this.notiunread); this.initializeApp(); } initializeApp() { this.platform.ready().then(() => { this.statusBar.styleBlackTranslucent(); this.splashScreen.hide(); const SigninData = JSON.parse(localStorage.getItem("signindata")); if(SigninData!=null){ console.log("have data"); this.router.navigate(["/tabs"], { skipLocationChange: true }); } else { console.log("no data"); this.router.navigate(["/landing"], { skipLocationChange: true }); } // this.fcm.subscribeToTopic('marketing'); this.fcm.getToken().then(token => { console.log(token); }, (err) => { console.log(err); }); // ionic push notification example this.fcm.onNotification().subscribe(data => { console.log(data); if (data.wasTapped) { console.log('Received in background'); this.router.navigate(['/tabs/tab1/notification']); } else { this.backgroundMode.enable(); this.badge.set(this.notiunread); console.log('Received in foreground'); this.call(data); } }); // refresh the FCM token this.fcm.onTokenRefresh().subscribe(token => { console.log(token); }, (err) => { console.log(err); }); }); this.providersservices.getObservable().subscribe((data) => { console.log('Data received', data); this.pic = data.pic; this.name = data.name; this.phone = data.number; }); const SigninData = JSON.parse(localStorage.getItem("signindata")); if(SigninData!=null){ const SigninData = JSON.parse(localStorage.getItem("signindata")); this.userData.id=SigninData.mem_id; this.userData.token=SigninData.token; this.userData.plid = "7"; this.providersservices.postData(this.userData, "memberencrypt").subscribe((result) =>{ this.data = result; this.items = this.data.data; this.code = this.items.mem_id; console.log(result); }, (err) => { console.log(JSON.stringify(err)); }); } this.providersservices.postData(this.userData, "badgenotification").subscribe((result) =>{ this.data3 = result; this.items3 = this.data3.data; this.notiunread = this.items3.length; this.backgroundMode.enable(); console.log(this.notiunread); this.badge.set(this.notiunread); if(Array.isArray(this.items3)) this.exist3 = this.items3[0].exist; else this.exist3 = this.items3.exist; console.log(result); }, (err) => { console.log(JSON.stringify(err)); }); this.providersservices.postData(this.userData, "pointshare").subscribe((result) =>{ this.data1 = result; this.items1 = this.data1.data; this.point = this.items1.pl_value; console.log(result); }, (err) => { console.log(JSON.stringify(err)); }); if(this.pic==undefined&&this.name==undefined&&this.name==undefined){ const SigninData = JSON.parse(localStorage.getItem("signindata")); if(SigninData!=null){ this.pic = SigninData.mem_photo; this.name = SigninData.mem_name; this.phone = SigninData.mem_phonetcode + SigninData.mem_phone; } } this.menuCtrl.enable(false, 'custom'); { //this.translate.addLangs(['en', 'ml']); //this.translate.setDefaultLang('en'); } }
问题根因与修复方案
核心原因
该问题由iOS APNs推送环境的强隔离机制导致:
- Xcode调试构建的应用走APNs沙盒(Sandbox)环境,使用Development签名证书,设备生成的是沙盒环境专属的FCM推送Token
- TestFlight、App Store安装的正式包走APNs生产(Production)环境,使用Distribution签名证书,设备生成的是生产环境专属的FCM推送Token
两类Token完全不互通。安装TestFlight版本后,FCM会将对应设备的旧沙盒Token标记为失效、替换为新的生产Token,此时再用旧的沙盒Token发推送,自然无法送达调试版应用。
分步修复
分环境管理推送Token
不要用同一条用户记录混存不同环境的推送Token。给上报的Token增加环境标识:- 调试阶段测试时,仅向标记为沙盒环境的Token发推送
- 发布TestFlight/正式包时,仅向标记为生产环境的Token发推送
同一个设备先后安装调试版、正式版时,要根据上报的环境类型分别存储对应Token,不要互相覆盖。
修正服务端推送参数错误
当前代码存在两处参数问题,直接影响推送送达率:sound字段层级错误:必须放在notification对象内部才能在iOS端触发提示音,写在最外层无效- 缺少高优先级标识:iOS端只有高优先级推送才能保证在后台/锁屏状态下稳定送达
修正后的$fields配置参考:
$fields = array( 'to' => $token, 'priority' => 'high', 'notification' => array( 'title' => $title, 'body' => $message, 'click_action' => 'FCM_PLUGIN_ACTIVITY', 'sound' => 'default' ), 'data' => array('message' => $message, 'title' => $title), );校验Xcode调试包签名与能力配置
打开Xcode项目逐项检查Debug配置:- 选中项目Target → Signing & Capabilities,确认Push Notifications能力已正常添加
- 确认Debug构建的APS Environment值为
development,Release构建的APS Environment值为production - Debug包必须使用Development证书签名,禁止误用Distribution证书签名调试版本
重新获取调试版有效Token
卸载测试设备上原有调试版、TestFlight版应用,重新通过Xcode安装Debug包,从控制台获取最新的沙盒环境FCM Token,单独使用该Token测试调试版推送,不要和正式环境Token混用。Firebase证书校验
登录Firebase控制台,确认iOS应用下同时上传了有效的APNs沙盒证书和生产证书,若仅上传生产证书,所有沙盒环境的推送请求会被FCM直接拒绝。
内容的提问来源于stack exchange,提问作者Eleen009
相关产品推荐
相关产品推荐

