【iOS,GCP】Walletに追加したPassをリモートからアップデートしたい。

サーバー側:GCP(Cloud Functions)
モバイル側:Flutter

・アプリのおおまかな作り
アプリで自分のお店のPassを作成 → PassデータがFirebase Storageに保存される → お客さんにFirebase StorageのURLを共有し、Passを取得してもらう
・やりたいこと
Pass情報を編集した時にWalletにAPNを送信してPassをアップデートしたい

12/1 9:00
これに沿って実装する。

まず、GCPのCloud FunctionsからWalletにプッシュ通知を送りたい。

これに沿って自前コードを書いてみたけど

export const onCardUpdated = functionFirestore
    .document("passes/{uid}")
    .onUpdate(async (snapshot, context) => {
        var passData = snapshot.after.data()
        var deviceSnap = await admin.firestore().collection('devices').where('serialNumber','==',passData.serialNumber).get()
        var devices = deviceSnap.docs
        console.log('devices', devices.length)
        
        for (var i in devices) {
            var pushToken = devices[i].data().pushToken
            console.log('send to ', pushToken)
            var path = "/3/device/"+pushToken
            var url = "https://api.push.apple.com:443"+path
            
            const response = await fetch(url, {
                method: 'POST',
                body: null,
                headers: {
                    'apns-topic': passId,
                    'apns-push-type': 'background',
                    'connection': 'keep-alive'
                }
            });
            
            if (!response.ok) {
                console.log('エラー!',response.json())
            }
            
            if (response.body !== null) {
                var json = response.json()
                console.log('json',json)
            }
        }
    })

内容のないエラーしか返ってこない。
TypeError: fetch failed
ダメもとでFlutterでもやってみたけど、これも中身のないエラーが返ってきた。
ClientException: Invalid request method

Appleさんに質問してみたけどサポート外ということだった。
Appleのプラットフォームにも半ギレでポストしてみたが、今日(2023/12/1)時点では反応なし。
https://developer.apple.com/forums/thread/742343
Axios使ってやってみても意味不明なエラーが出て、Githubのissuesに挙げてみたが3日間無反応。Axios開発者からしたら「こっちの問題じゃねーよ」って感じだろう。

URLリクエストは単純なPOSTくらいしかしたことないんだけど、そこが間違ってそう。APNs用の証明書を送る工程が必要なんじゃないか?わからない。

10:04

Cloud Messagingを使って送れるかを試す。

トピックをPass Type IDにすると送信できるよー。的なことを言っている人がいた。

解釈が正しいか定かではないので、まずCloud Messagingのコンソールから送ってみた。

届かない。

そもそもトピックにサブスクライブしたユーザーに通知送る時は、アプリ側からサブスクライブする必要なかったっけ?
今回はPassをWalletに追加した人に通知を送りたいので、アプリをインストールしていない場合が多い。したがってアプリからサブスクライブというのができない。

どうしたらいいんだ。。

10:50

トピックから送る方法は諦めて、Certificateを使おうと思った。
The notification uses the same certificate and private key that the creator of the pass used to sign the original
とドキュメントには書いてあるが、そもそもcertificateをどこで使えばいいのかわからない。

アプリをインストールしてない人にでもAPNを送れるのか、ここからCode-Level Supportでお問い合わせしてみた。
https://developer.apple.com/contact/technical/#!/request/form

返信を待つ。

12/7 13:55
1週間経ったがAppleから返信が来ない。
Apple Developer Forumにも投稿してみたけど誰も返信してくれない。
質問内容を変えて、必死な感じでもう1つTSI送ってみた。

次の年 01/11

何も進展してない。
Code Level Supportから返信が来て、アプリをインストールしてない人にでもAPNは送れるらしい。
そしてCode Level Supportはバックエンドはサポートできないらしいので、やはりStackoverFlowとかに頼るしかない。

質問投稿してみた。
https://stackoverflow.com/questions/77797994/how-to-send-a-push-notification-to-ios-wallet-app-using-typescript-firebase

同じような質問はApple Develper Forumにいくつかあるのに、誰も答えていない。いったいみんなどうやって実装してるんだ。

1/13

地道に調べて実装できた!!!

var path = "/3/device/"+pushToken
            var url = "https://api.push.apple.com:443"
            console.log(url)

            var storage = admin.storage().bucket()
            var signerCert = (await getRawBody(storage.file('signerCert.pem').createReadStream())).toString();
            var signerKey = (await getRawBody(storage.file('signerKey.pem').createReadStream())).toString();
            
            const clientSessionOptions: http2.ClientSessionOptions = {};
            const secureClientSessionOptions: http2.SecureClientSessionOptions = Object.assign({}, clientSessionOptions);
            secureClientSessionOptions.cert = signerCert;
            secureClientSessionOptions.key = signerKey;
            secureClientSessionOptions.passphrase = 'test';
            const client = http2.connect(url, secureClientSessionOptions);
            
            client.on('socketError', (err) => console.error('ソケットエラー!',err));
            client.on('error', (err) => console.error('エラー!',err));
            const payloadString = JSON.stringify({});
            const req = client.request({ 
                ':method': 'POST',
                ':path': path,
                'apns-push-type': 'background',
                'content-type': 'application/json',
                'content-length': Buffer.byteLength(payloadString, 'utf-8').toString(),
            });
            req.on('response', (headers, flags) => {
                for (const name in headers) {
                    console.log(`レスポンス ${name}: ${headers[name]}`);
                }
            });
            req.write(payloadString);
            req.setEncoding('utf8');
            let data = '';
            req.on('data', (chunk) => { data += chunk; });
            req.on('end', () => {
                console.log(`\n終了 ${data}`);
                client.destroy();
            });
            console.log('Http2.終了する')
            req.end();

ヤッターーーーーーーーー!!!!!!!

この記事が気に入ったらサポートをしてみませんか?