mcpField
G
gplay-purchase-verification

gplay-purchase-verification

github.com/tamtom開発52351 回インストール14 週間前に更新
リポジトリ

gplay-purchase-verification について

gplay-purchase-verificationは、Google Playのアプリ内購入やサブスクリプションをサーバー側で検証する開発者向けスキルです。購入の承認・消費、状態や通知の処理に加え、gplayコマンドとバックエンドの実装例を扱います。

主な機能

  1. 01アプリ内購入の詳細取得
  2. 02サブスクリプションと状態の検証
  3. 03購入の承認または消費
  4. 04解約・延期・取り消しの管理
  5. 05RTDN通知の設定とデコード

活用シーン

  1. 01サーバーでトークンを検証して権利を付与
  2. 02状態を確認して更新や解約に対応
  3. 03返金・取り消し・無効購入を処理

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/tamtom/gplay-cli-skills --skill gplay-purchase-verification

インストール後、Claude Code で/gplay-purchase-verificationを入力して読み込まれたか確認します。

詳細

Google Play の購入検証

バックエンドサーバーからアプリ内購入やサブスクリプションを検証する必要がある場合に、このスキルを使用します。

サーバー側で購入を検証する理由

クライアント側の検証は回避される可能性があります。必ずサーバーで購入を検証してください。

  • 不正行為や海賊版を防止する
  • ユーザーが実際に支払いを済ませたことを確認する
  • サブスクリプションのステータスを確認する
  • 返金や解約に対応する

認証の設定

購入を検証するには、バックエンドに権限を持つサービスアカウントが必要です。

サービスアカウントを作成する
  1. Google Cloud Console に移動します
  2. サービスアカウントを作成します
  3. 「Service Account User」ロールを付与します
  4. JSON キーをダウンロードします
API アクセスを許可する
  1. Play Console に移動します
  2. ユーザーと権限 → サービスアカウント
  3. サービスアカウントにアプリへのアクセス権を付与します

アプリ内商品の購入を検証する

購入の詳細を取得する
gplay purchases products get \
  --package com.example.app \
  --product-id premium_upgrade \
  --token <PURCHASE_TOKEN>
レスポンス
{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": "1706400000000",
  "purchaseState": 0,
  "consumptionState": 0,
  "developerPayload": "user_123",
  "orderId": "GPA.1234-5678-9012-34567",
  "purchaseType": 0
}
購入状態
  • 0 = 購入済み
  • 1 = キャンセル済み
  • 2 = 保留中
消費状態
  • 0 = 未消費
  • 1 = 消費済み

購入を承認する

検証後、購入を承認します。

gplay purchases products acknowledge \
  --package com.example.app \
  --product-id premium_upgrade \
  --token <PURCHASE_TOKEN>

重要: 承認されていない購入は、3 日後に返金されます。

購入を消費する(消費型アイテムの場合)

消費型アイテム(コイン、ジェムなど)の場合:

gplay purchases products consume \
  --package com.example.app \
  --product-id coins_100 \
  --token <PURCHASE_TOKEN>

サブスクリプションを検証する

v2 API を推奨します。 新しい統合では、gplay purchases subscriptionsv2 get (および subscriptionsv2 cancel/defer/revoke、purchases productsv2 get)を使用してください。v2 の SubscriptionPurchaseV2 モデルは基本プランと特典を反映します。以下の v1 エンドポイントも 引き続き動作しますが、旧形式です。

サブスクリプションの詳細を取得する(v2、推奨)
gplay purchases subscriptionsv2 get \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN>
サブスクリプションの詳細を取得する(v1、旧形式)
gplay purchases subscriptions get \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN>
レスポンス
{
  "kind": "androidpublisher#subscriptionPurchase",
  "startTimeMillis": "1706400000000",
  "expiryTimeMillis": "1709000000000",
  "autoRenewing": true,
  "priceCurrencyCode": "USD",
  "priceAmountMicros": "4990000",
  "paymentState": 1,
  "cancelReason": null,
  "userCancellationTimeMillis": null,
  "orderId": "GPA.1234-5678-9012-34567",
  "linkedPurchaseToken": null,
  "subscriptionState": 0
}
サブスクリプションの状態
  • 0 = 有効
  • 1 = キャンセル済み(有効期限までは有効)
  • 2 = 猶予期間中
  • 3 = 保留中(支払いに失敗し、再試行中)
  • 4 = 一時停止中
  • 5 = 期限切れ
支払い状態
  • 0 = 支払い保留中
  • 1 = 支払い受領済み
  • 2 = 無料トライアル
  • 3 = アップグレード/ダウングレードの延期が保留中

バックエンドの実装例

Node.js/Express
const { google } = require('googleapis');

async function verifyPurchase(packageName, productId, token) {
  const auth = new google.auth.GoogleAuth({
    keyFile: '/path/to/service-account.json',
    scopes: ['https://www.googleapis.com/auth/androidpublisher'],
  });

  const androidpublisher = google.androidpublisher({
    version: 'v3',
    auth: await auth.getClient(),
  });

  const result = await androidpublisher.purchases.products.get({
    packageName: packageName,
    productId: productId,
    token: token,
  });

  return result.data;
}

// Endpoint
app.post('/verify-purchase', async (req, res) => {
  const { packageName, productId, token } = req.body;

  try {
    const purchase = await verifyPurchase(packageName, productId, token);

    if (purchase.purchaseState === 0) {
      // Purchase is valid
      // Grant access to user
      // Acknowledge purchase
      res.json({ valid: true, purchase });
    } else {
      res.json({ valid: false });
    }
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
Python/Flask
from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/androidpublisher']
SERVICE_ACCOUNT_FILE = '/path/to/service-account.json'

credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_FILE, scopes=SCOPES)

androidpublisher = build('androidpublisher', 'v3', credentials=credentials)

@app.route('/verify-purchase', methods=['POST'])
def verify_purchase():
    data = request.json
    package_name = data['packageName']
    product_id = data['productId']
    token = data['token']

    try:
        result = androidpublisher.purchases().products().get(
            packageName=package_name,
            productId=product_id,
            token=token
        ).execute()

        if result['purchaseState'] == 0:
            # Purchase is valid
            return jsonify({'valid': True, 'purchase': result})
        else:
            return jsonify({'valid': False})

    except Exception as e:
        return jsonify({'error': str(e)}), 400

サブスクリプションイベントに対応する

リアルタイムデベロッパー通知(RTDN)

サブスクリプションイベントを受信するように Pub/Sub を設定します。

  1. Google Cloud Console で Pub/Sub トピックを作成します
  2. Play Console で設定します:
    • 収益化の設定 → リアルタイムデベロッパー通知
    • トピック名を入力します

gplay を使うと、base64/JSON の解析を手作業で書かずに、RTDN のスキャフォールドを生成してデコードできます。

# Print the Pub/Sub topic + Play Console setup steps
gplay rtdn setup --package com.example.app

# Decode an RTDN payload into readable JSON (notification type, token, etc.)
# Accepts a full Pub/Sub envelope (message.data is base64) or the raw notification.
gplay rtdn decode --data '{"message":{"data":"<BASE64_DATA>"}}'
cat payload.json | gplay rtdn decode --file -
  1. イベントをサブスクライブします:
from google.cloud import pubsub_v1

subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(project_id, subscription_id)

def callback(message):
    data = json.loads(message.data)

    if 'subscriptionNotification' in data:
        notification = data['subscriptionNotification']
        notification_type = notification['notificationType']
        purchase_token = notification['purchaseToken']

        # Handle different events
        if notification_type == 1:  # SUBSCRIPTION_RECOVERED
            # Subscription was recovered from account hold
            pass
        elif notification_type == 2:  # SUBSCRIPTION_RENEWED
            # Subscription renewed successfully
            pass
        elif notification_type == 3:  # SUBSCRIPTION_CANCELED
            # User canceled subscription
            pass
        elif notification_type == 4:  # SUBSCRIPTION_PURCHASED
            # New subscription purchase
            pass
        elif notification_type == 7:  # SUBSCRIPTION_EXPIRED
            # Subscription expired
            pass
        elif notification_type == 10:  # SUBSCRIPTION_PAUSED
            # Subscription paused
            pass
        elif notification_type == 12:  # SUBSCRIPTION_REVOKED
            # Subscription revoked (refunded)
            pass

    message.ack()

subscriber.subscribe(subscription_path, callback=callback)

サブスクリプション管理

サブスクリプションをキャンセルする
gplay purchases subscriptions cancel \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN>
サブスクリプションを延期する
gplay purchases subscriptions defer \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN> \
  --json @defer.json
defer.json
{
  "deferralInfo": {
    "expectedExpiryTimeMillis": "1709000000000"
  }
}
サブスクリプションを取り消す(返金)
gplay purchases subscriptions revoke \
  --package com.example.app \
  --token <SUBSCRIPTION_TOKEN>

無効化された購入を確認する

返金/キャンセルされた購入の一覧を取得します。

gplay purchases voided list \
  --package com.example.app \
  --start-time 1706400000000 \
  --end-time 1709000000000

バックエンドで、これらの購入に対する権限を削除してください。

注文情報

注文の詳細を取得する
gplay orders get \
  --package com.example.app \
  --order-id GPA.1234-5678-9012-34567
複数の注文を取得する
gplay orders batch-get \
  --package com.example.app \
  --order-ids "GPA.1234,GPA.5678,GPA.9012"
注文を返金する

orders refund は破壊的な書き込み操作であり、--confirm が必要です。これがない場合、コマンドは実行を拒否します。

gplay orders refund \
  --package com.example.app \
  --order-id GPA.1234-5678-9012-34567 \
  --revoke \    # Also revoke entitlement/access
  --confirm     # Required — refund is irreversible

セキュリティのベストプラクティス

推奨事項:
  • ✅ 必ずサーバーで検証し、クライアントを決して信頼しない
  • ✅ 購入トークンを安全に保管する
  • ✅ 3 日以内に購入を承認する
  • ✅ 返金やキャンセルに対応する
  • ✅ すべての API 呼び出しに HTTPS を使用する
  • ✅ 検証エンドポイントにレート制限を設ける
  • ✅ すべての検証試行をログに記録する
禁止事項:
  • ❌ クライアント上だけで購入を検証する
  • ❌ サービスアカウントの認証情報を公開する
  • ❌ 購入の承認を省略する
  • ❌ 検証前にアクセス権を付与する
  • ❌ 無効化された購入を無視する
  • ❌ クレジットカード情報を保存する(PCI 準拠)

一般的な検証フロー

  1. アプリでユーザーが購入します
  2. アプリが購入トークンをサーバーに送信します
  3. サーバーがGoogle Play API で検証します
  4. (有効な場合)サーバーが購入を確認します
  5. サーバーがユーザーにアクセス権/コンテンツを付与します
  6. サーバーが今後の確認に備えて購入トークンを保存します
  7. サーバーがRTDN イベント(キャンセル、更新)をリッスンします

エラー処理

よくあるエラー
  • 401 Unauthorized - サービスアカウントが認可されていません
  • 404 Not Found - 購入トークンが無効または期限切れです
  • 410 Gone - 購入が払い戻しまたはキャンセルされています
再試行ロジック
async function verifyWithRetry(packageName, productId, token, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await verifyPurchase(packageName, productId, token);
    } catch (error) {
      if (error.code === 404 || error.code === 410) {
        throw error; // Don't retry if purchase is invalid
      }
      if (i === retries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
}

テスト

テスト購入

Google Play のテストアカウントを使うと、実際に課金されることなくテスト購入を行えます。

検証テスト
# Verify test purchase
gplay purchases products get \
  --package com.example.app \
  --product-id android.test.purchased \
  --token <TEST_TOKEN>

モニタリング

以下の指標を追跡します。

  • 購入検証の成功率
  • 購入確認率
  • 払い戻し率
  • サブスクリプションの解約率
  • 決済失敗率

このデータを活用して、収益化戦略を改善してください。

コメント