本文へスキップ
Unveilydocs

アプリ内課金 (IAP)

Google Play および App Store のアプリ内課金・サブスクリプションをJavaScriptで連携します。

概要

Google Play Billing と App Store StoreKit 2 を WebView から JavaScript で制御します。 商品照会・購入・復元・完了確認(Acknowledge)を提供し、サーバー側レシート検証はお客様のWebサーバーを経由して 実行します(下記 サーバー検証 を参照)。

メソッド説明
queryProductsストアの商品リストと価格を取得
purchase購入フローを開始
acknowledgePurchase購入確認(Android 専用、3日以内に必須)
restorePurchases過去の購入を復元(再インストール時等)

対応プラン: Pro

プラットフォーム実装
AndroidGoogle Play Billing
iOSStoreKit 2

購入フローの順序

正しい順序: queryProductspurchaseサーバー検証(お客様のWebサーバー → Unveily)acknowledgePurchase(Android のみ)

サーバー検証はレシート偽造防止のために必須であり、アプリではなくお客様のWebサーバーが Unveily API(/api/iap/verify)を呼び出します。SDK には専用の serverVerify メソッドはありません — 購入結果をお客様のWebサーバーへ送信して検証します。詳細は下記 サーバー検証 セクションを参照してください。 iOS(StoreKit 2)は purchase() 完了時にトランザクションが自動的に finish 処理されるため、acknowledgePurchase の呼び出しは不要です。


queryProducts

ストアに登録された商品の詳細と現在の価格を取得します。 Android は Google Play 商品 ID、iOS は App Store Connect 商品 ID を使用します。

window.unveilyBridge.iap.queryProducts(
  ["monthly_pro", "yearly_pro"],
  "subs",
  "onProductsLoaded"
);

function onProductsLoaded(result) {
  const { products, error } = result;
  if (error) { console.error("取得失敗:", error); return; }
  products.forEach(p => console.log(`${p.title}: ${p.price}`));
}

レスポンス

{
  "products": [
    {
      "productId": "monthly_pro",
      "title": "Pro 月額プラン",
      "description": "すべての Pro 機能をご利用いただけます",
      "type": "subs",
      "price": "¥980",
      "priceAmountMicros": 980000000,
      "priceCurrencyCode": "JPY"
    }
  ]
}

purchase

ストアの購入画面を表示します。ユーザーが購入を完了またはキャンセルするとコールバックが呼ばれます。

window.unveilyBridge.iap.purchase("monthly_pro", "subs", "onPurchaseResult");

function onPurchaseResult(result) {
  if (!result.success) {
    if (result.cancelled) return;
    console.error("購入失敗:", result.error);
    return;
  }
  // 購入成功 → お客様のWebサーバーへ送信してサーバー検証(下記「サーバー検証」を参照)
  // Android: result.purchaseToken / iOS: result.signedTransaction
  verifyOnYourServer(result);
}

レスポンス

Android 成功時:

{
  "success": true,
  "productId": "monthly_pro",
  "purchaseToken": "purchase_token...",
  "orderId": "GPA.1234-5678",
  "purchaseTime": 1713456789000,
  "purchaseState": 1
}

iOS 成功時:

{
  "success": true,
  "productId": "monthly_pro",
  "transactionId": "2000000123456789",
  "purchaseTime": 1713456789000,
  "signedTransaction": "<Apple署名トランザクション (JWS)>"
}

キャンセル時: { "success": false, "cancelled": true }
エラー時: { "success": false, "error": "エラーメッセージ" }

サーバー検証に使う値

サーバー検証では Android は purchaseTokeniOS は signedTransaction(Apple が署名した JWS)をお客様のWebサーバーへ送信します。iOS の transactionId は識別用であり、偽造検証は署名された signedTransaction で実行されます。

サブスクリプションのアップグレード / ダウングレード

既存のサブスクリプションを別のものへ変更(アップグレード・ダウングレード)する場合は、purchase にオプションオブジェクトを追加で渡すオーバーロードを使用します。オプションオブジェクトはコールバック名の に置きます。

window.unveilyBridge.iap.purchase(
  "yearly_pro",                     // 新しい商品 ID
  "subs",                           // サブスクリプション
  {
    oldPurchaseToken: "既存の購入トークン",  // 現在のサブスクの purchaseToken
    replacementMode: 1                     // 差額(プロレーション)モード
  },
  "onPurchaseResult"                // コールバック関数名
);

プラットフォームの違い

このオプションオブジェクトは Android(Google Play Billing) でのみ使用されます。iOS はオプションオブジェクトを黙って無視 します — App Store のサブスクリプショングループがアップグレード・ダウングレードを自動的に処理するためです。

replacementMode の値(Android):

モード説明
1WITH_TIME_PRORATIONデフォルト。残り期間を新価格で再計算
2CHARGE_PRORATED_PRICE差額を即時請求
3WITHOUT_PRORATION次回更新時に新価格を適用
5CHARGE_FULL_PRICE全額を即時請求
6DEFERRED現在のサブスク満了後に変更を適用

機能ゲーティング — coming_soon ステータス

アプリ内課金は、ライセンス機能 "iap"(Pro)とサーバー側の iapEnabled フラグの 両方 が有効な場合にのみ動作します。まだ有効化されていない場合、queryProductspurchase{ "status": "coming_soon" } を返すため、Webアプリでこのステータスを処理して案内 UI を表示してください。

function onPurchaseResult(result) {
  if (result.status === "coming_soon") {
    alert("アプリ内課金は近日提供予定です。");
    return;
  }
  // ... 通常処理
}

サーバー検証(お客様のWebサーバー経由)

レシートの偽造・二重請求を防ぐため、サーバー検証は必須です。SDK は Unveily を直接呼び出しません — 購入結果を お客様のWebサーバー へ送信し、お客様のWebサーバーが Unveily API(/api/iap/verify)を呼び出します。(ほとんどのWebアプリはすでにWebサーバーを持っているため、別途 IAP サーバーを構築する必要はありません。)

purchase 成功後、(Android は)acknowledgePurchase の前に検証してください。 サーバー検証なしで機能を提供すると、レシート偽造に脆弱になります。

流れ

アプリ(SDK 購入) → 購入結果 → Webコンテンツがお客様のWebサーバーへ送信
   → お客様のWebサーバー → Unveily POST /api/iap/verify → 検証結果
プラットフォーム別の送信値:
  - Android: purchaseToken + 短期 googleAccessToken (Webサーバーがサービスアカウントで生成)
  - iOS:     signedTransaction (Apple署名 JWS) — 認証情報不要

1) Webアプリ — 購入結果を自社サーバーへ送信

// purchase コールバック(onPurchaseResult)から呼び出す
async function verifyOnYourServer(result) {
  const payload = result.purchaseToken
    ? { platform: "android", productId: result.productId, productType: "subs",
        purchaseToken: result.purchaseToken }
    : { platform: "ios", productId: result.productId, productType: "subs",
        signedTransaction: result.signedTransaction };

  const res = await fetch("/api/verify-iap", {            // お客様のWebサーバーのエンドポイント
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  const verify = await res.json();
  if (!verify.success) { alert("決済の検証に失敗しました。サポートへお問い合わせください。"); return; }

  unlockProFeatures();
  // Android のみ Acknowledge(iOS は StoreKit 2 が自動処理)
  if (result.purchaseToken) {
    window.unveilyBridge.iap.acknowledgePurchase(result.purchaseToken, "onAckResult");
  }
}

2) お客様のWebサーバー — Unveily を呼び出す(relay)

認証情報(Google サービスアカウント)は お客様のWebサーバーにのみ 置き、Unveily には保存しません。

// 例: Node.js / Express
import { GoogleAuth } from "google-auth-library";

const UNVEILY_LICENSE_KEY = process.env.UNVEILY_LICENSE_KEY;

app.post("/api/verify-iap", async (req, res) => {
  const { platform, productId, productType, purchaseToken, signedTransaction } = req.body;
  const body = { licenseKey: UNVEILY_LICENSE_KEY, platform, productId, productType };

  if (platform === "android") {
    body.packageName = process.env.ANDROID_PACKAGE_NAME;
    body.purchaseToken = purchaseToken;
    body.googleAccessToken = await getGoogleAccessToken();  // ↓ 短期トークンを生成
  } else {
    body.bundleId = process.env.IOS_BUNDLE_ID;
    body.signedTransaction = signedTransaction;             // Apple署名 JWS をそのまま転送
  }

  const r = await fetch("https://api.actuallyworks.net/api/iap/verify", {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  res.status(r.status).json(await r.json());
});

// サービスアカウント JSON で短期 access token を生成(長期キーはサーバー外へ出さない)
async function getGoogleAccessToken() {
  const auth = new GoogleAuth({
    keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_JSON_PATH,
    scopes: ["https://www.googleapis.com/auth/androidpublisher"],
  });
  const client = await auth.getClient();
  const { token } = await client.getAccessToken();
  return token;
}

セキュリティ

  • 短期トークンを送信してください — サービスアカウント JSON(長期キー)はお客様のWebサーバーの外へ出さないでください。上記のように ~1時間有効の access token のみを生成して渡します。
  • トークン・署名トランザクションを ログに残さないでください。
  • すべての通信は HTTPS。licenseKey はサーバーの環境変数で保管してください(アプリに露出禁止)。

Unveily レスポンス形式(/api/iap/verify

{
  "success": true,
  "data": {
    "productId": "monthly_pro",
    "orderId": "GPA.1234-5678",
    "purchaseTime": 1713456789000,
    "isAcknowledged": false,
    "storeApiVerified": true
  }
}

エラー時: { "success": false, "message": "エラーメッセージ" }

storeApiVerified フィールド

storeApiVerified: true — Google Play API(Android)または Apple 署名検証(iOS)によるリアルタイム検証が完了
storeApiVerified: false — Android で googleAccessToken が未送信の場合(DB 記録のみ実行)

Google Play サービスアカウントの設定は IAP 連携ガイド を参照してください。iOS は認証情報なしでサーバーが Apple 署名(JWS)を検証します。


acknowledgePurchase

購入を確認します。Google Play は3日以内に Acknowledge されない場合、自動返金処理を行います。

Android 専用 — 3日以内に必須

このメソッドは Android(Google Play) 専用です。 iOS(StoreKit 2)は purchase() 完了時にトランザクションが自動的に finish 処理されるため、別途呼び出しは不要です。

Android では、サーバー検証が完了した後に必ず acknowledgePurchase を呼び出してください。 呼び出しを省略すると、Google が自動返金してサブスクリプションをキャンセルします。

window.unveilyBridge.iap.acknowledgePurchase(purchaseToken, "onAckResult");

function onAckResult(result) {
  if (result.success) console.log("購入確認完了");
  else console.error("確認失敗:", result.error);
}

パラメーター

パラメーター説明
purchaseTokenstringpurchase で受け取った購入トークン(Android)
callbackstring結果を受け取るグローバル関数名

成功: { "success": true }
エラー: { "success": false, "error": "エラーメッセージ" }


restorePurchases

再インストールや機種変更後に過去の購入を復元します。 アプリ起動時、またはユーザーが「購入を復元」ボタンをタップしたときに呼び出します。

  • Android: Google Play のアクティブなサブスクリプションリストを返します。
  • iOS: StoreKit 2 の Transaction.currentEntitlements をもとに復元します。
window.unveilyBridge.iap.restorePurchases("subs", "onRestoreResult");

function onRestoreResult(result) {
  const { purchases, error } = result;
  if (error) { console.error("復元失敗:", error); return; }
  const active = (purchases || []).filter(p => p.isAcknowledged || p.transactionId);
  if (active.length > 0) unlockProFeatures();
}

レスポンス

Android:

{
  "purchases": [
    {
      "productId": "monthly_pro",
      "purchaseToken": "...",
      "orderId": "GPA.1234-5678",
      "purchaseTime": 1713456789000,
      "purchaseState": 1,
      "isAcknowledged": true
    }
  ]
}

iOS:

{
  "purchases": [
    {
      "productId": "monthly_pro",
      "transactionId": "2000000123456789",
      "purchaseTime": 1713456789000,
      "isAcknowledged": true,
      "signedTransaction": "<Apple署名トランザクション (JWS)>"
    }
  ]
}

実装全体の例

// ── グローバル状態 ─────────────────────────────────────
let currentProductId = null;

// ── 1. 起動時に既存のサブスクリプションを復元 ──────────
window.addEventListener("load", () => {
  if (window.unveilyBridge?.iap) {
    window.unveilyBridge.iap.restorePurchases("subs", "onRestoreResult");
  }
});

// ── 2. 購入ボタン ───────────────────────────────────────
function startSubscription(productId) {
  currentProductId = productId;
  window.unveilyBridge.iap.purchase(productId, "subs", "onPurchaseResult");
}

// ── 3. 購入結果 → お客様のWebサーバーで検証(relay)──────
window.onPurchaseResult = async function(result) {
  if (!result.success) return;
  // Android: purchaseToken / iOS: signedTransaction(JWS) を自社サーバーへ送信
  const payload = result.purchaseToken
    ? { platform: "android", productId: currentProductId, productType: "subs",
        purchaseToken: result.purchaseToken }
    : { platform: "ios", productId: currentProductId, productType: "subs",
        signedTransaction: result.signedTransaction };

  const res = await fetch("/api/verify-iap", {   // お客様のWebサーバーが Unveily を呼び出す
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  const verify = await res.json();
  if (!verify.success) {
    alert("決済の検証に失敗しました。サポートへお問い合わせください。");
    return;
  }
  unlockProFeatures();
  // Android のみ Acknowledge が必要(iOS は StoreKit 2 が自動処理)
  if (result.purchaseToken) {
    window.unveilyBridge.iap.acknowledgePurchase(result.purchaseToken, "onAckResult");
  }
};

// ── 5. Acknowledge 完了 ─────────────────────────────────
window.onAckResult = function(result) {
  if (result.success) console.log("サブスクリプション有効化完了");
};

// ── 復元処理 ─────────────────────────────────────────────
window.onRestoreResult = function(result) {
  const active = (result.purchases || []).filter(
    p => p.isAcknowledged || p.transactionId
  );
  if (active.length > 0) unlockProFeatures();
};

function unlockProFeatures() {
  // Pro 機能の UI を有効化
  document.body.classList.add("pro-user");
}

関連ドキュメント

On this page