アプリ内課金 (IAP)
Google Play および App Store のアプリ内課金・サブスクリプションをJavaScriptで連携します。
概要
Google Play Billing と App Store StoreKit 2 を WebView から JavaScript で制御します。 商品照会・購入・復元・完了確認(Acknowledge)を提供し、サーバー側レシート検証はお客様のWebサーバーを経由して 実行します(下記 サーバー検証 を参照)。
| メソッド | 説明 |
|---|---|
queryProducts | ストアの商品リストと価格を取得 |
purchase | 購入フローを開始 |
acknowledgePurchase | 購入確認(Android 専用、3日以内に必須) |
restorePurchases | 過去の購入を復元(再インストール時等) |
対応プラン: Pro
| プラットフォーム | 実装 |
|---|---|
| Android | Google Play Billing |
| iOS | StoreKit 2 |
購入フローの順序
正しい順序: queryProducts → purchase → サーバー検証(お客様の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 は purchaseToken、iOS は 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):
| 値 | モード | 説明 |
|---|---|---|
1 | WITH_TIME_PRORATION | デフォルト。残り期間を新価格で再計算 |
2 | CHARGE_PRORATED_PRICE | 差額を即時請求 |
3 | WITHOUT_PRORATION | 次回更新時に新価格を適用 |
5 | CHARGE_FULL_PRICE | 全額を即時請求 |
6 | DEFERRED | 現在のサブスク満了後に変更を適用 |
機能ゲーティング — coming_soon ステータス
アプリ内課金は、ライセンス機能 "iap"(Pro)とサーバー側の iapEnabled フラグの 両方 が有効な場合にのみ動作します。まだ有効化されていない場合、queryProducts・purchase は { "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);
}パラメーター
| パラメーター | 型 | 説明 |
|---|---|---|
purchaseToken | string | purchase で受け取った購入トークン(Android) |
callback | string | 結果を受け取るグローバル関数名 |
成功: { "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");
}関連ドキュメント
- IAP 連携ガイド — Google Play Console と App Store Connect の設定
- アプリ情報 Bridge — ライセンスプランの確認