Apple Developer News の「Get your subscriptions ready for iOS 27」を読んでいたとき、本文の一行で手が止まりました。サブスクリプションの複数シート購入は、App Store Connect ですでに既定で有効になっている、という記述です。
自分で何かを押した覚えはありません。それでも設定は変わっていて、10月22日に Volume Purchasing が始まれば、その設定がそのまま「売れる形」になります。ここでは、Rork Max で作ったネイティブアプリでも、Expo 側で作ったアプリでも変わらない、残すか外すかの決め方を整理します。
変わったのは機能ではなく、既定値でした
Apple の記述から確認できる範囲を、日付で並べます。細部は App Store Connect の画面で必ずご自身の目でも確かめてください。
| 日付 | 起きること | こちらがやること |
| 9月16日 | サブスクの複数シート購入が既定で有効になっている。無効化や、販路ごとの制御は可能 | 各サブスクの現在の状態を見る |
| 10月22日 | Volume Purchasing の開始。前提は StoreKit 2 | 残す・外すを、この日までに決める |
| 今冬 | Group Purchases の開始予定 | 今回の判断記録をそのまま使い回せるようにしておく |
| 年内 | Bundles and Suites は申請フォーム制で予定 | 当面は様子を見る |
ここで気づいたのは、「何もしない」ことも一つの決定になってしまうという点です。既定値が有効である以上、放置すれば「残す」を選んだのと同じ結果になります。
何もしないことも、決めたことにしておく。 今回の作業は、この一文を実行に移すだけです。
残すか外すかを決める4つの問い
私は、サブスクごとに次の4つを順に自問します。どれも、App Store Connect の画面ではなく自分のアプリの側にある事情です。
- そのサブスクの価値は、一人の端末の中で完結しているか。 壁紙の広告非表示のように、個人の体験に紐づくものは、組織が複数人分まとめて買う場面を想像しにくいはずです。逆に、共有できる成果物や設定を持つものは、まとめ買いの需要があり得ます。
- 価格は、シート単位で売っても成り立つか。 個人向けに設計した月額は、まとめて買われる前提で値付けしていません。割引や管理の手間が、まとめ買いのぶんだけ増える可能性を見ておきます。
- 問い合わせを受ける体力があるか。 組織の購入は、領収書の宛名や担当者の変更といった個人向けにはなかった質問を連れてきます。個人開発では、ここがいちばん効きます。
- アプリ側の権限判定は、「買った人=使う人」で書かれていないか。 次の節で確かめます。
4つのうち、3つ以上が「いいえ」「分からない」なら、その時点では外す側に倒します。外したあとで再び有効にするのは簡単ですが、売れ始めたものを引っ込めるのは、購入者への説明を伴うからです。
権限判定を一か所に寄せる
私は旧 SKPayment から StoreKit 2 へ移したとき、「いま有料機能を使ってよいか」の判定が画面ごとに散らばっていたことに気づきました。Volume Purchasing は StoreKit 2 が前提とされているので、散らばった判定は、この機会に一か所へ寄せるのが安全です。
次のコードは、購入者の違いに依存せず、現在有効な権限だけを返す最小の形です。
import StoreKit
enum Access: Equatable {
case none
case active(productID: String, ownership: Transaction.OwnershipType, expires: Date?)
}
struct EntitlementResolver {
let proProductIDs: Set<String>
func current() async -> Access {
var best: Access = .none
for await result in Transaction.currentEntitlements {
// 検証に通らない取引は、有料機能の根拠にしない
guard case .verified(let tx) = result else { continue }
guard proProductIDs.contains(tx.productID) else { continue }
// 返金・取り消しされた取引と、期限切れを外す
guard tx.revocationDate == nil else { continue }
if let exp = tx.expirationDate, exp < Date.now { continue }
best = .active(productID: tx.productID,
ownership: tx.ownershipType,
expires: tx.expirationDate)
}
return best
}
}
なぜこう書くか。判定の根拠を Transaction.currentEntitlements の一本に絞ると、誰が買ったかという個別の事情が、画面側のコードへ漏れなくなります。ownershipType は取り出して保持するだけにしてあり、分岐には使っていません。後から「家族共有は別扱いにしたい」と決めたとき、直す場所が一つで済みます。
更新を拾う側も、一つにまとめておきます。
func startListening(onChange: @escaping @Sendable () async -> Void) -> Task<Void, Never> {
Task.detached {
for await update in Transaction.updates {
if case .verified(let tx) = update {
await tx.finish()
await onChange()
}
}
}
}
Transaction.updates は、アプリが閉じている間に起きた購入や失効も流してくれます。ここを持っていないと、別の端末で起きた変更が、次に画面を開くまで反映されません。
外した場合と残した場合を StoreKit Test で確かめる
設定を決めたら、決めた側の挙動を手元で確かめます。StoreKit Configuration ファイルをテストに読み込ませると、購入と失効を再現できます。
import StoreKitTest
import XCTest
final class EntitlementTests: XCTestCase {
var session: SKTestSession!
override func setUpWithError() throws {
session = try SKTestSession(configurationFileNamed: "Products")
session.resetToDefaultState()
session.clearTransactions()
session.disableDialogs = true
}
func testActiveAfterPurchase() async throws {
_ = try await session.buyProduct(productIdentifier: "pro.monthly")
let access = await EntitlementResolver(proProductIDs: ["pro.monthly"]).current()
guard case .active = access else { return XCTFail("購入直後なのに権限が無い") }
}
func testNoneAfterExpire() async throws {
_ = try await session.buyProduct(productIdentifier: "pro.monthly")
try session.expireSubscription(productIdentifier: "pro.monthly")
let access = await EntitlementResolver(proProductIDs: ["pro.monthly"]).current()
XCTAssertEqual(access, .none)
}
}
このテストが確かめているのは、「買った直後は有効、失効したら無効」という、誰が買っても変わらない約束だけです。まとめ買い固有の挙動を、ここで再現できるとは思わないでください。Apple 側の新しい購入形態は、実際に動き出すまで、サンドボックスでも見えない部分が残ります。私はそこを推測で埋めず、10月22日以降の実際の取引を見てから判断を直すつもりでいます。
二つの典型で、4つの問いを当ててみる
抽象的なままだと決めにくいので、個人開発でよくある二つの型に当てはめます。数字は使わず、答えの向きだけを見てください。
| 問い | 広告非表示型(個人の体験に閉じる) | 成果物や設定を共有できる型 |
| 1. 価値は一人で完結するか | はい | いいえ |
| 2. シート単位でも価格が成り立つか | 分からない | 分からない |
| 3. 問い合わせを受ける体力があるか | いいえ | いいえ |
| 4. 判定コードが集約済みか | はい | はい |
| 私の暫定の向き | 外す | 残して様子を見る |
直感に反していたのは、右の型でした。まとめ買いの需要がありそうだから残す、と決めたくなりますが、問い3が「いいえ」のままだと、売れた分だけ問い合わせが増えるだけです。そこで、右の型は「残す」を選びつつ、見直し日を早めに置きます。需要の有無より、受け止める体制のほうが先に効いてきます。
もう一つ、外す側にも落とし穴があります。外すという判断は安全に見えますが、権限判定が散らばったままだと、後で戻したときに画面ごとの不整合が出ます。外す・残すの前に、判定を一か所へ寄せておく。順番を間違えないことが、この作業のいちばんの近道でした。
判断を1枚の記録に残す
設定は、数か月たつと「なぜこうしたのか」が思い出せなくなります。ですから、サブスクごとに1つずつ、次の形で残します。
# decisions/multiseat-2026-10.yaml
decided_on: 2026-10-01
review_on: 2026-11-15 # 実際の取引を見て見直す日
products:
- id: pro.monthly
multiseat: off # on / off
reasons:
q1_value_is_personal: true
q2_price_works_per_seat: unknown
q3_support_capacity: false
q4_entitlement_code_ready: true # EntitlementResolver に集約済み
note: "2が unknown のため、外す側に倒した。見直し日に取引を見て再判断"
unknown を許したことが、この記録のいちばん大事な部分です。分からないものを「いいえ」と偽って書くと、見直し日に何を確かめるべきかが消えてしまいます。
落とし穴は、確かめた範囲を広く言ってしまうこと
最後に、本番で踏みやすい落とし穴と、その回避の考え方を3つ書き残します。最初に推奨したいのは、「確かめた範囲」と「推測」を文章の中で分けておくことです。私の場合は、記録の書式にその欄を設けるところから始めています。
- Apple の記述を、自分の言葉で広げないようにしています。 「既定で有効」「無効化や販路ごとの制御が可能」「前提は StoreKit 2」。確かめられたのはこの範囲で、細かい課金や返金の挙動は、公式の説明が増えてから書き足します。
- 外すと決めても、画面側の導線は残しておきます。 権限判定を一か所に寄せておけば、設定を変えてもアプリの更新は要りません。判断を後から変えられることが、決めやすさにつながります。
- Rork で作ったアプリの場合は、課金まわりを担っている層を先に確かめます。 Rork Max が生成するネイティブの Swift アプリは StoreKit 2 を直接使います。一方、RevenueCat のような課金サービスを挟んでいる構成では、そのサービス側の対応状況も確認が要ります。継続率や課金率の見え方が変わる可能性もあるので、ダッシュボードの数字は10月22日の前後で分けて見るつもりです。
次の一歩
今日のうちに、App Store Connect でサブスクを1つだけ開き、複数シート購入の欄がどうなっているかを見てください。そのうえで、上の4つの問いに「はい・いいえ・分からない」で答えてみてください。それだけで、10月22日は「慌てる日」から「見直しの日」に変わります。
私も、まずはいちばん小さなサブスクから、この記録を書き始めるつもりです。