App Store Connect の審査でリジェクトされた後、修正からの再提出までの全工程を API で実行しました。ブラウザでの操作は一切行っていません。

リジェクトの内容

JPS Explorer(ジャパンサーチ文化資源探索アプリ)の初回提出で、2つの問題を指摘されました。

  1. チップ(Tip Jar)画面でエラーが表示される — In-App Purchase の商品が App Store Connect に未登録だったため
  2. カメラ検索の「撮影」ボタンでクラッシュ — iOS の Info.plist に NSCameraUsageDescription が未設定だったため

修正内容

カメラクラッシュの修正

Info.plist にカメラと写真ライブラリの権限記述を追加しました。

<key>NSCameraUsageDescription</key>
<string>文化資源に似た画像を検索するためにカメラを使用します</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>文化資源に似た画像を検索するために写真ライブラリを使用します</string>

Flutter の image_picker パッケージを使ってカメラにアクセスする場合、この記述がないと実機でクラッシュします。シミュレータではカメラが使えないため、この問題には気づきにくいようです。

合わせて、PlatformExceptioncamera_access_deniedphoto_access_denied のハンドリングも追加しました。

チップ画面の修正

In-App Purchase の商品が未登録の場合、StoreKit がエラーを返します。エラーをそのまま表示するのではなく、「準備中です」というメッセージに変更しました。

API による再提出の手順

リジェクト後の状態では、App Store Connect のブラウザに「編集」ボタンと「App Reviewに再提出」ボタンが表示されます。これらの操作はすべて API で実行できます。

Step 1: ビルド番号を上げてアップロード

リジェクトされたビルドと同じビルド番号では再アップロードできないため、pubspec.yaml のビルド番号を上げます。

# 変更前
version: 1.0.0+1
# 変更後
version: 1.0.0+2

ビルドしてアップロードします。

flutter build ipa --export-method app-store
xcrun altool --upload-app --type ios \
  --file build/ios/ipa/jps_explorer.ipa \
  --apiKey $ASC_KEY_ID --apiIssuer $ASC_ISSUER_ID

Step 2: ビルドの処理完了を待つ

アップロード後、Apple 側でビルドの処理が行われます。processingStateVALID になるまでポーリングします。

builds = api_request("GET",
    f"builds?filter[app]={APP_ID}&sort=-uploadedDate&limit=1")
state = builds["data"][0]["attributes"]["processingState"]
# PROCESSING → VALID

通常は数分で完了しますが、ビルド番号が同じだと新しいビルドとして認識されないことがあります。

Step 3: 新しいビルドを紐付ける

NEW_BUILD_ID = builds["data"][0]["id"]

api_request("PATCH",
    f"appStoreVersions/{VERSION_ID}/relationships/build",
    {"data": {"type": "builds", "id": NEW_BUILD_ID}})

Step 4: 暗号化コンプライアンスを設定

新しいビルドごとに暗号化の申告が必要です。

api_request("PATCH", f"builds/{NEW_BUILD_ID}", {
    "data": {
        "type": "builds", "id": NEW_BUILD_ID,
        "attributes": {"usesNonExemptEncryption": False}
    }
})

既に設定済みのビルドに再度設定すると 409 ENTITY_ERROR.ATTRIBUTE.INVALID エラー(「You cannot update when the value is already set」)が返ります。このエラーは無視して問題ないようです。

Step 5: スクリーンショットを差し替える(任意)

リジェクト後であればスクリーンショットの削除・追加が可能です。審査中(WAITING_FOR_REVIEW)の状態では変更できなかったスクリーンショットが、リジェクト後は編集可能になります。

# 既存のスクリーンショットを削除
api_request("DELETE", f"appScreenshots/{screenshot_id}")

# 新しいスクリーンショットをアップロード(3段階)
# 1. 予約
result = api_request("POST", "appScreenshots", {...})
# 2. バイナリアップロード
# 3. コミット
api_request("PATCH", f"appScreenshots/{screenshot_id}", {...})

Step 6: 再提出

リジェクト時のサブミッションID(UNRESOLVED_ISSUES 状態)に対して submitted: true を送信します。

api_request("PATCH", f"reviewSubmissions/{SUBMISSION_ID}", {
    "data": {
        "type": "reviewSubmissions",
        "id": SUBMISSION_ID,
        "attributes": {"submitted": True}
    }
})

新しいサブミッションを作成する必要はありません。既存のサブミッションを再利用します。

タイミングに関する注意点

「Version is not ready to be submitted yet」エラー

再提出時に以下のエラーが返ることがあります。

{
  "code": "STATE_ERROR",
  "detail": "Version is not ready to be submitted yet, please try again later."
}

これはビルドの紐付けやスクリーンショットの処理が内部的に完了していないことを示しているようです。数分待ってから再試行すれば解決しました。

App Store Connect のブラウザで「審査準備完了」と表示されるタイミングと、API で提出可能になるタイミングにずれがある場合があります。今回は App Store Connect のブラウザで確認した後に API から再提出が成功しました。

古いサブミッションの扱い

複数回提出を試みると READY_FOR_REVIEW 状態のサブミッションが溜まります。これらは canceled: true でキャンセルできる場合がありますが、今回は 409 エラーでキャンセルできませんでした。最終的に UNRESOLVED_ISSUES 状態のサブミッションに対して再提出することで解決しています。

リジェクト後のフロー(API操作のみ)

1. コード修正
2. pubspec.yaml のビルド番号を +1
3. flutter build ipa
4. xcrun altool --upload-app
5. builds API でビルド処理完了を待つ (VALID)
6. PATCH appStoreVersions/{id}/relationships/build で新ビルド紐付け
7. PATCH builds/{id} で暗号化コンプライアンス設定
8. (任意)スクリーンショット差し替え
9. PATCH reviewSubmissions/{id} submitted: true で再提出

ブラウザでの「編集」ボタンや「App Reviewに再提出」ボタンの操作は不要です。ただし、API でのタイミングの問題がある場合は、ブラウザで状態を確認するのが確実です。

今回得られた知見

項目詳細
Info.plist のカメラ権限NSCameraUsageDescription 未設定でカメラアクセス時にクラッシュする。シミュレータでは発見できない
IAP 商品未登録のエラー表示StoreKit のエラーをそのまま表示せず、ユーザー向けのメッセージに変換するのがよさそう
ビルド番号の重複同じビルド番号では再アップロードされない。+1 する必要がある
暗号化コンプライアンスの再設定新ビルドごとに必要。既設定のビルドへの再設定は 409 エラーになるが無視可能
再提出のタイミングビルド紐付け直後は「not ready」になることがある。数分待つと成功する
サブミッションの再利用リジェクト時の UNRESOLVED_ISSUES サブミッションに対して再提出が可能