واجهة JavaScript bridge API
المشتريات والاستحقاقات
البيع من صفحتك، ومعرفة ما يملكه هذا المشتري بالفعل. راجع دليل المشتريات داخل التطبيق للاطلاع على الشيفرة المحيطة بذلك.
AppMint.revenuecat دالة مساعدة (helper)#
AppMint.revenuecat — RevenueCat subscriptions
مثال
الاشتراكات والمشتريات لمرة واحدة عبر مشروع RevenueCat الخاص بك: العروض والمشتريات والاستعادات والاستحقاقات. تتم المشتريات عبر Google Play في التطبيق؛ ويسجّل RevenueCat كل واحدة قبل أن ينهيها التطبيق.
يعيد: كل طريقة تعيد Promise يُحل دائمًا بـ {ok: true, …} أو {ok: false, error, message?}. configure({appUserId?})، getCustomerInfo()، restore()، logIn(id)، logOut() ← {ok, customerInfo}؛ getOfferings() ← {ok, current, all}؛ purchase(pkgOrProductId, {offeringId?, offerId?, consumable?}) ← {ok, productId, transaction, customerInfo}؛ getAppUserId() ← {ok, appUserId}. يتطلب: خطوة التكامل ← RevenueCat بمفتاح Google Play العام الخاص بك (goog_…).
if (window.AppMint && AppMint.revenuecat) {
await AppMint.revenuecat.configure({ appUserId: currentUser && currentUser.id }); // omit for an anonymous user
var off = await AppMint.revenuecat.getOfferings();
if (off.ok && off.current) {
off.current.availablePackages.forEach((p) => addPlanButton(p.identifier, p.title, p.price, p.period));
}
}
async function buy(pkg) { // a package object from getOfferings, or its identifier
var r = await AppMint.revenuecat.purchase(pkg);
if (r.ok) unlock(r.customerInfo.entitlements.active);
else if (r.error === 'pending') alert('Payment pending — it will unlock when Google confirms it.');
else if (!r.cancelled) alert('Purchase failed: ' + (r.message || r.error));
}
var info = await AppMint.revenuecat.getCustomerInfo();
var isPro = info.ok && !!info.customerInfo.entitlements.active.pro;
window.addEventListener('appmint:revenuecat', (e) => render(e.detail.customerInfo)); // entitlements changed
document.getElementById('restore').onclick = () => AppMint.revenuecat.restore();اربط المشتريات بحسابك الخاص:
if (window.AppMint && AppMint.revenuecat) {
var li = await AppMint.revenuecat.logIn(user.id); // after sign-in
if (li.ok) refreshPlan(li.customerInfo.entitlements.active);
document.getElementById('signout').onclick = async function () {
await AppMint.revenuecat.logOut();
showAnonymous();
};
}اعرض رسالة صادقة لكل فشل:
async function buyPackage(pkg) {
var r = await AppMint.revenuecat.purchase(pkg);
if (r.ok) return unlock(r.customerInfo.entitlements.active);
if (r.cancelled || r.error === 'cancelled') return;
var text = {
not_configured: 'Purchases are not set up in this build',
billing_unavailable: 'Google Play billing is not available on this phone',
product_not_found: 'This plan is not available right now',
network: 'No connection - try again online'
}[r.error] || ('Purchase failed: ' + (r.message || r.error));
alert(text);
}ملاحظات: customerInfo = {appUserId, originalAppUserId, requestDate, managementUrl, entitlements: {active, all}, activeSubscriptions, allPurchasedProductIds, nonSubscriptionTransactions}؛ كل استحقاق {identifier, isActive, productIdentifier, expirationDate, latestPurchaseDate, willRenew, periodType, store, isSandbox}. مرّر {consumable: true} للعملات/الأرصدة لتُشترى مجددًا؛ أما الاشتراكات وعمليات الفتح لمرة واحدة فتُقَرّ. دون اتصال، يجيب getCustomerInfo بآخر معلومات معروفة مع stale: true؛ والشراء الذي يتم بينما RevenueCat غير متاح يُنهى مع ذلك ويُرسل إيصاله عند configure/restore التالي (receiptQueued: true). الأخطاء: not_configured، network، revenuecat_error (مع status، code، message)، billing_unavailable، billing_error، product_not_found، offer_not_found، cancelled، pending، already_anonymous، foreign_origin. أعدّ اتصال حساب خدمة Google Play مع RevenueCat كما يطلب RevenueCat؛ ولا تضع أبدًا مفتاح sk_ سريًا في تطبيق. تظل مشتريات AppMintIAP الحالية تعمل دون تغيير.
AppMintIAP.available دالة مساعدة (helper)#
window.AppMintIAP.available
مثال
خاصية (وليست دالة): true عندما تستطيع هذه الصفحة الشراء فعلًا عبر Google Play.
يعيد: خاصية منطقية. يتطلب: 💰 بيع منتجات داخل التطبيق. بدونه لا يوجد window.AppMintIAP إطلاقًا.
تحقق منه قبل عرض أي شيء للبيع، وقل بوضوح عندما يتعذر الشراء هنا:
var inApp = !!(window.AppMintIAP && window.AppMintIAP.available);
var buyBtn = document.getElementById('buy-pro');
if (!inApp) {
buyBtn.disabled = true;
buyBtn.textContent = 'Available in the Android app';
}زر ترقية واحد لموقعك وتطبيقك: Google Play داخل التطبيق، وصفحة الدفع الخاصة بك على الويب:
document.getElementById('upgrade').addEventListener('click', function () {
var iap = window.AppMintIAP;
if (iap && iap.available) {
iap.buy('pro_unlock'); // inside the app
} else if (window.WebToApk) {
showMessage('Upgrades are not available in this version of the app.');
} else {
location.href = '/checkout/pro'; // your website's own checkout
}
});في React أو Next.js، اقرأه داخل تأثير (العرض على الخادم بلا window)، فلا يُعرض شيء للشراء خارج التطبيق:
import { useEffect, useState } from 'react';
export function useCanBuy() {
const [canBuy, setCanBuy] = useState(false); // false during the server render
useEffect(() => {
setCanBuy(!!(window.AppMintIAP && window.AppMintIAP.available));
}, []);
return canBuy;
}
// const canBuy = useCanBuy();
// return canBuy ? <UpgradeButton /> : null;ملاحظات: يُثبَّت window.AppMintIAP قبل تشغيل أول سكربت في صفحتك؛ وفقط في WebView الأقدم من 2021 تقريبًا يصل بعد انتهاء تحميل الصفحة، لذا ينبغي للشيفرة التي يجب أن تعمل هناك أيضًا أن تنتظره (انظر نمط whenIap في AppMintIAP.sell). القيمة نفسها موجودة في AppMintIAP.state().available. لفّ العرض في data-iap-container لإخفائه خارج التطبيق دون أي شيفرة.
AppMintIAP.buy دالة مساعدة (helper)#
window.AppMintIAP.buy(id)
مثال
يفتح نافذة شراء Google Play لمنتج، من زرك الخاص. لا يمنح شيئًا بمفرده.
يعيد: لا شيء. تصل النتيجة كـ appmint:purchase {productId, owned:true} (وعبر AppMintIAP.onChange) أو appmint:purchase-failed {productId, reason}. يتطلب: 💰 بيع منتجات داخل التطبيق وأن يكون المنتج نشطًا في Play Console.
استخدمه عندما ترسم الزر بنفسك؛ يأتي الفتح من الملكية، لا من النقر:
var btn = document.getElementById('buy-pro');
btn.addEventListener('click', function () {
if (!window.AppMintIAP || !window.AppMintIAP.available) { showMessage('Available in the Android app.'); return; }
btn.disabled = true;
window.AppMintIAP.buy('pro_unlock');
});
window.addEventListener('appmint:purchase', function (e) {
if (e.detail.productId === 'pro_unlock') unlockPro(); // must be safe to run twice
btn.disabled = false;
});
window.addEventListener('appmint:purchase-failed', function (e) {
btn.disabled = false;
showMessage(e.detail.reason === 'pending' ? 'Payment pending. It unlocks once Google confirms.' : 'Purchase failed: ' + e.detail.reason);
});
document.addEventListener('visibilitychange', function () { if (!document.hidden) btn.disabled = false; }); // cancel: no eventمنتقي خطط بزر "متابعة" واحد يشتري الخطة المحددة:
document.getElementById('continue').addEventListener('click', function () {
var choice = document.querySelector('input[name="plan"]:checked'); // value = product ID
var iap = window.AppMintIAP;
if (!choice || !iap || !iap.available) return;
if (iap.isOwned(choice.value)) { showMessage('You already have this plan.'); return; }
iap.buy(choice.value);
});
window.addEventListener('appmint:purchase', function () {
document.getElementById('plan-picker').hidden = true;
showMessage('Thanks! Your plan is active.');
});النقر على أداة Pro فقط يشغّلها إذا كانت مملوكة، وإلا يفتح نافذة الشراء:
document.querySelectorAll('[data-pro-tool]').forEach(function (btn) {
btn.addEventListener('click', function () {
var iap = window.AppMintIAP;
if (iap && iap.isOwned('pro_unlock')) { runTool(btn.getAttribute('data-pro-tool')); return; }
if (iap && iap.available) iap.buy('pro_unlock');
else showMessage('Pro tools are available in the Android app.');
});
});
window.addEventListener('appmint:purchase', function (e) {
if (e.detail.productId === 'pro_unlock') document.body.classList.add('pro');
});ملاحظات: buy() هو WebToApk.purchase(id) المجرد: بلا تسمية انشغال ولا فحص مجموعة. الأزرار المربوطة بـ sell() أو start() أو data-iap-buy تفعل كل ذلك نيابةً عنك، ففضّلها.
AppMintIAP.isOwned دالة مساعدة (helper)#
window.AppMintIAP.isOwned(id)
مثال
يتحقق فورًا ودون اتصال مما إذا كان المستخدم يملك منتجًا (منتج لمرة واحدة، أو اشتراك لا يزال نشطًا).
يعيد: true / false، بشكل متزامن؛ وfalse خارج التطبيق. يتطلب: 💰 بيع منتجات داخل التطبيق.
function paintPro() {
var pro = !!(window.AppMintIAP && window.AppMintIAP.isOwned('pro_unlock'));
document.getElementById('pro-tools').hidden = !pro;
}
// Re-check whenever ownership can change.
window.addEventListener('appmint:purchase', paintPro);
window.addEventListener('appmint:owned-changed', paintPro);
// AppMintIAP arrives after the page has loaded: paint once it is there.
var waitIap = setInterval(function () {
if (window.AppMintIAP) { clearInterval(waitIap); paintPro(); }
}, 200);
setTimeout(function () { clearInterval(waitIap); }, 10000); // not in the app, or selling is offPro عبر أي من عدة خطط: منتج شهري وسنوي ومدى الحياة تُحتسب جميعها:
var PRO_PLANS = ['pro_monthly', 'pro_yearly', 'pro_lifetime'];
function isPro() {
var iap = window.AppMintIAP;
return !!iap && PRO_PLANS.some(function (id) { return iap.isOwned(id); });
}
function paintPlan() { document.body.classList.toggle('pro', isPro()); }
paintPlan();
window.addEventListener('appmint:purchase', paintPlan);
window.addEventListener('appmint:owned-changed', paintPlan); // an expired subscription or a refundحارس مسار: الشاشات الخاصة بـ Pro توجّه المستخدم بلا Pro إلى شاشة الترقية:
function route() {
var path = location.hash.slice(1) || '/';
var proOnly = path.indexOf('/reports') === 0;
if (proOnly && !(window.AppMintIAP && window.AppMintIAP.isOwned('pro_unlock'))) {
location.replace('#/upgrade');
return;
}
render(path);
}
window.addEventListener('hashchange', route);
route();ملاحظات: الإجابة نفسها كـ WebToApk.isOwned(id). تُستعاد الملكية من حساب Google للمستخدم عند كل تشغيل وتُحفظ على الهاتف، فتبقى بعد إعادة التثبيت وتعمل دون اتصال. لا تحتفظ بالاستحقاق أبدًا في localStorage.
AppMintIAP.onChange دالة مساعدة (helper)#
window.AppMintIAP.onChange(fn)
مثال
يستدعي دالتك بحالة الشراء فورًا، ومجددًا كلما تغيرت (وصلت الأسعار، تم شراء، الاستعادة عند التشغيل، استرداد، انتهاء اشتراك).
يعيد: دالة لإلغاء الاشتراك. تستلم fn القيمة {available, adFree, owned, ownedIds, products}؛ وproducts هي {id: {price, currency, type, period, owned}} للمنتجات التي يعرفها المساعد (من sell أو start أو data-iap-buy). يتطلب: 💰 بيع منتجات داخل التطبيق.
خطّاف React. يعيد دالة إلغاء الاشتراك مباشرة من التأثير:
import { useEffect, useState } from 'react';
type IapState = { available: boolean; adFree: boolean; owned: boolean; ownedIds: string[];
products: Record<string, { price: string | null; currency: string | null; type: string | null; period: string | null; owned: boolean }> };
export function useIap(ids: string[]) {
const [iap, setIap] = useState<IapState>({ available: false, adFree: false, owned: false, ownedIds: [], products: {} });
useEffect(() => {
let stop: (() => void) | undefined;
const t = setInterval(() => { // AppMintIAP arrives after page load
const api = (window as any).AppMintIAP;
if (!api) return;
clearInterval(t);
ids.forEach((id) => api.sell(id, null)); // track them, so their prices are fetched
stop = api.onChange(setIap);
}, 200);
return () => { clearInterval(t); if (stop) stop(); };
}, []);
return iap;
}
// const iap = useIap(['pro_unlock']);
// const isPro = iap.ownedIds.includes('pro_unlock');
// const price = iap.products.pro_unlock?.price;جدار دفع بـ JavaScript عادي: تتبع خطتين بلا أزرار، ثم ارسم أسعارهما والخطة الحالية عند كل تغيير:
var iap = window.AppMintIAP;
var PLANS = ['pro_monthly', 'pro_yearly'];
if (iap) {
iap.start({ plans: PLANS.map(function (id) { return { id: id }; }) }); // tracked, not drawn
iap.onChange(function (s) {
document.getElementById('paywall').hidden = !s.available;
PLANS.forEach(function (id) {
var el = document.querySelector('[data-plan="' + id + '"]');
var p = s.products[id];
el.hidden = !p; // Google has not answered for it (yet)
if (p) el.querySelector('.price').textContent = p.owned ? 'Your plan' : p.price;
});
});
}اشترك أثناء فتح شاشة الحساب وألغِ الاشتراك عند إغلاقها. يُملأ adFree وownedIds بمجرد تشغيل المساعد (تستخدم الصفحة sell() أو start() أو data-iap-buy):
var stopAccount = null;
function openAccountScreen() {
if (window.AppMintIAP) {
stopAccount = window.AppMintIAP.onChange(function (s) {
document.getElementById('ad-status').textContent = s.adFree ? 'Ad-free' : 'With ads';
document.getElementById('owned-count').textContent = s.ownedIds.length + ' purchase(s)';
});
}
showScreen('account');
}
function closeAccountScreen() {
if (stopAccount) { stopAccount(); stopAccount = null; } // no updates for a hidden screen
showScreen('home');
}ملاحظات: اقرأ الملكية لكل منتج (ownedIds أو products[id].owned). الخاصية العليا owned تعني فقط "أي شيء مملوك". adFree هو استحقاق إزالة الإعلانات (WebToApk.isPremium()). لا يدخل منتج في products إلا بعد أن يجيب Google عنه.
AppMintIAP.owned دالة مساعدة (helper)#
window.AppMintIAP.owned()
مثال
يسرد كل معرّف منتج يملكه المستخدم.
يعيد: مصفوفة معرّفات منتجات، بشكل متزامن، مثل ['pro_unlock', 'level_pack_2']؛ و[] خارج التطبيق أو عندما لا يوجد شيء مملوك. يتطلب: 💰 بيع منتجات داخل التطبيق.
function unlockOwnedLevels() {
var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
document.querySelectorAll('[data-level-pack]').forEach(function (el) {
el.classList.toggle('locked', ids.indexOf(el.getAttribute('data-level-pack')) === -1);
});
}
window.addEventListener('appmint:owned-changed', unlockOwnedLevels); // restore or refund changed the list
window.addEventListener('appmint:purchase', unlockOwnedLevels);احسب عدد المستويات المفتوحة عندما تكون كل حزمة مستويات منتجًا مستقلًا:
function unlockedLevelCount() {
var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
var packs = ids.filter(function (id) { return /^level_pack_\d+$/.test(id); }).length;
return 10 + packs * 10; // 10 free levels, 10 more per pack
}
function paintLevelCount() {
document.getElementById('levels-label').textContent = unlockedLevelCount() + ' levels unlocked';
}
paintLevelCount();
window.addEventListener('appmint:owned-changed', paintLevelCount);قائمة "مشترياتي" في شاشة إعداداتك، باسمك الخاص لكل منتج:
var LABELS = { pro_unlock: 'Pro features', remove_ads: 'No ads', level_pack_2: 'Levels 11-20' };
function renderPurchases() {
var ids = window.AppMintIAP ? window.AppMintIAP.owned() : [];
var list = document.getElementById('purchases');
list.textContent = '';
ids.forEach(function (id) {
var li = document.createElement('li');
li.textContent = LABELS[id] || id;
list.appendChild(li);
});
document.getElementById('no-purchases').hidden = ids.length > 0;
}
renderPurchases();
window.addEventListener('appmint:owned-changed', renderPurchases);ملاحظات: يُحلَّل من WebToApk.getOwnedProducts() نيابةً عنك. يشمل الاشتراكات النشطة ومنتج إزالة الإعلانات. يعمل دون اتصال.
AppMintIAP.refresh دالة مساعدة (helper)#
window.AppMintIAP.refresh()
مثال
يبحث مجددًا عن أزرار data-iap-buy التي أُضيفت منذ آخر فحص، ويربطها (أو يبدأ المساعد إذا لم يجد أيًا منها بعد).
يعيد: الكائن AppMintIAP. يتطلب: 💰 بيع منتجات داخل التطبيق.
يراقب المساعد الصفحة أصلًا بحثًا عن أزرار data-iap-buy الجديدة، لذا نادرًا ما تحتاج هذا. استدعِه بعد إدراج أزرار بطريقة قد يفوتها المراقب، مثل تغيير سمات عنصر موجود:
function showShop() {
var btn = document.getElementById('buy-coins');
btn.setAttribute('data-iap-buy', 'coins_500'); // attribute added to an EXISTING node
btn.setAttribute('data-iap-label', '500 coins — {price}');
if (window.AppMintIAP) window.AppMintIAP.refresh();
}بطاقات متجر تأتي منتجاتها من خادمك: الأزرار موجودة بالفعل في الصفحة، وتتغير سماتها فقط، فاربطها بـ refresh():
fetch('/shop.json').then(function (r) { return r.json(); }).then(function (shop) {
shop.items.forEach(function (item) { // [{slot, productId, name}]
var btn = document.querySelector('.shop-card[data-slot="' + item.slot + '"] button');
if (!btn) return;
btn.setAttribute('data-iap-buy', item.productId);
btn.setAttribute('data-iap-label', item.name + ' - {price}');
});
if (window.AppMintIAP) window.AppMintIAP.refresh();
});حزمة عطلة تُطرح للبيع في وقت محدد، على زر كان في الصفحة من البداية:
var saleStart = new Date('2026-12-01T00:00:00').getTime();
function openSale() {
var btn = document.getElementById('sale-btn');
btn.setAttribute('data-iap-buy', 'holiday_bundle');
btn.setAttribute('data-iap-owned', 'Bundle unlocked');
document.getElementById('sale-card').hidden = false;
if (window.AppMintIAP) window.AppMintIAP.refresh();
}
var wait = saleStart - Date.now();
if (wait <= 0) openSale();
else if (wait < 86400000) setTimeout(openSale, wait); // later than a day: the next launch opens itملاحظات: يحصل كل زر جديد على سعره من Google وحالة امتلاكه. الزر المربوط بالفعل لا يُربط مرتين. الأزرار التي أُزيل عنصرها من الصفحة تُسقَط.
AppMintIAP.sell دالة مساعدة (helper)#
window.AppMintIAP.sell(id, button, opts)
مثال
يربط منتجًا واحدًا بزر واحد في سطر واحد. استدعِه مرة لكل منتج.
يعيد: الكائن AppMintIAP (للتسلسل). يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء، وأن يكون المنتج مُنشأً ونشطًا في Play Console. يُثبَّت window.AppMintIAP قبل تشغيل أول سكربت في صفحتك (في WebView الأقدم من 2021 تقريبًا، بعد انتهاء تحميل الصفحة - والنمط whenIap أدناه يغطي الحالتين).
function whenIap(fn) {
if (window.AppMintIAP) { fn(window.AppMintIAP); return; }
var tries = 0;
var t = setInterval(function () {
if (window.AppMintIAP) { clearInterval(t); fn(window.AppMintIAP); }
else if (++tries > 50) clearInterval(t); // not in the app, or selling is off
}, 200);
}
whenIap(function (iap) {
iap.sell('pro_unlock', '#buy-pro', {
label: 'Unlock Pro — {price}', // {price} = Google's live, localized price
owned: '✓ Unlocked', // the button text once owned
show: '#thanks', // shown once owned
hide: '.upsell' // hidden once owned
});
iap.sell('coins_500', '#buy-coins', { label: '500 coins — {price}' });
});عدة خطط يحل فيها شراء واحدة محل الأخرى تشترك في group:
whenIap(function (iap) {
iap.sell('pass_monthly', '#monthly', { label: 'Monthly — {price}', group: 'pass' })
.sell('pass_yearly', '#yearly', { label: 'Yearly — {price}', group: 'pass' });
});تتبع منتجًا بلا زر وارسم العرض بنفسك (null لا يربط شيئًا؛ وزرك يستدعي buy):
whenIap(function (iap) {
iap.sell('remove_ads', null); // tracked, never drawn by the helper
iap.onChange(function (s) {
var p = s.products.remove_ads; // undefined until Google answers
var btn = document.getElementById('remove-ads');
if (!s.available || (p && p.owned)) { btn.hidden = true; return; }
btn.hidden = false;
btn.textContent = p && p.price ? 'Remove ads for ' + p.price : 'Remove ads';
});
document.getElementById('remove-ads').addEventListener('click', function () {
iap.buy('remove_ads');
});
});اربط زرًا لا يوجد إلا بعد فتح مربع حوار متجرك (الاستدعاء المتأخر ينضم إلى المساعد الجاري):
document.getElementById('open-shop').addEventListener('click', function () {
openShopDialog(); // your code: adds <button id="buy-levels">
if (!window.AppMintIAP) return; // not in the app
window.AppMintIAP.sell('level_pack_2', '#buy-levels', {
label: 'Levels 11-20 — {price}',
owned: '✓ Levels 11-20 unlocked',
show: '#levels-2-link' // your menu entry, shown once owned
});
});ملاحظات: button عنصر أو محدد CSS؛ مرّر null لتتبع منتج بلا زر (واجهتك تقرأ AppMintIAP.state()). opts الأخرى: labelEl (العنصر داخل الزر الذي يتغير نصه). يعمل قبل بدء المساعد أو بعده، بما في ذلك من تأثير React. وهو مثل سمات data-iap-buy (انظر AppMintIAP.start). افتح المحتوى المدفوع فقط بناءً على الملكية (isOwned، onChange)، وليس أبدًا في معالج النقر الخاص بك.
AppMintIAP.start دالة مساعدة (helper)#
window.AppMintIAP.start(config) and the data-iap-* attributes
مثال
يربط أزرار الشراء بـ Google Play في خطوة واحدة: أسعار حية على الأزرار، ونافذة الشراء عند النقر، وتسميات "مملوك"، وكتل شكر، واستعادة عند التشغيل، ورسالة ظاهرة لكل فشل.
يعيد: الكائن AppMintIAP (للتسلسل). يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء، وأن تكون معرّفات المنتجات مُنشأة ونشطة في Play Console. يثبّت التطبيق window.AppMintIAP قبل تشغيل أول سكربت في صفحتك (في WebView الأقدم من 2021 تقريبًا، فقط بعد انتهاء تحميل الصفحة).
بلا أي شيفرة: ضع علامات في HTML. يقرأ المساعد هذه السمات بنفسه فور حقنه، ويراقب الأزرار التي يعرضها React أو Vue لاحقًا:
<div data-iap-container> <!-- shown only inside the app -->
<button data-iap-buy="pass_monthly" data-iap-group="pass"
data-iap-label="Monthly — {price}" data-iap-owned="Active">Monthly</button>
<button data-iap-buy="pass_lifetime" data-iap-group="pass"
data-iap-label="Lifetime — {price}" data-iap-owned="Owned">
<img src="star.svg" alt=""> <span data-iap-text>Lifetime</span> <!-- only this text changes -->
</button>
<button data-iap-buy="coins_500" data-iap-label="500 coins — {price}">500 coins</button>
</div>
<p data-iap-show="pass" hidden>You are a member. Thank you!</p> <!-- shown when any "pass" plan is owned -->
<p data-iap-hide="pass">Members get every level and no ads.</p> <!-- hidden once it is owned -->بالشيفرة: استدعِ start() بنفسك عندما تريد دالة رسائل خاصة بك أو خطّافات فتح. استخدمه في الصفحات التي لا تحتوي على أزرار data-iap-buy في HTML (معها يكون المساعد قد بدأ بالفعل، وبدء ثانٍ لا يفعل شيئًا):
function whenIap(fn) { // AppMintIAP arrives after page load
if (window.AppMintIAP) { fn(window.AppMintIAP); return; }
var tries = 0;
var t = setInterval(function () {
if (window.AppMintIAP) { clearInterval(t); fn(window.AppMintIAP); }
else if (++tries > 50) clearInterval(t); // not in the app, or selling is off
}, 200);
}
whenIap(function (iap) {
iap.start({
plans: [
{ id: 'pro_unlock', button: '#buy-pro', label: 'Unlock Pro — {price}', owned: '✓ Pro' },
{ id: 'coins_500', button: '#buy-coins', label: '500 coins — {price}' }
],
show: '#pro-area', // shown once anything is owned
hide: '.upsell', // hidden once anything is owned
busyLabel: 'Opening Google Play…',
onUnlock: function (id) { console.log('now owned:', id); },
onLock: function (id) { console.log('no longer owned (refund or expiry):', id); },
toast: function (msg) { showMessage(msg); } // failure messages; default: window.showToast, else alert
});
});في React: ابدأ بخطط بلا button (لا يُرسم شيء) ودع React يعرض من onChange الذي يعيد دالة إلغاء الاشتراك للتأثير:
function ProButton() {
const [iap, setIap] = React.useState({ available: false, products: {} });
React.useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return; // server render / browser
window.AppMintIAP.start({ plans: [{ id: 'pro_unlock' }] }); // safe to run twice
return window.AppMintIAP.onChange(setIap);
}, []);
const pro = iap.products?.pro_unlock; // read ownership per product
if (!iap.available) return null; // nothing to buy outside the app
return pro?.owned
? <p>Thanks! Pro is unlocked.</p>
: <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>Unlock Pro {pro?.price}</button>;
}ملاحظات: السمات: data-iap-buy="id"، data-iap-label (يصبح {price} سعر Play الحي)، data-iap-owned (تسمية بعد الامتلاك؛ الافتراضي "✓ Purchased")، data-iap-group (خطط يحل بعضها محل بعض؛ بعد امتلاك واحدة تختفي الأخرى ما لم يكن hideOtherPlans: false)، data-iap-show / data-iap-hide (القيمة = معرّف المنتج أو المجموعة؛ الفارغ = أي شيء مملوك)، data-iap-text (الجزء من الزر الذي يتغير نصه)، data-iap-container. يحصل الزر المملوك على data-iap-state="owned" والفئة iap-unlocked. الإلغاء في نافذة Google لا يرسل حدثًا؛ ويُحرَّر الزر عندما تظهر الصفحة مجددًا (أو بعد 30 ثانية). استدعاء start(options) بعد أن بدأ المساعد من تلقاء نفسه من سمات data-iap-buy يدمج الخيارات (الخطط، الإظهار/الإخفاء، الحاوية، onUnlock/onLock، toast، busyLabel، hideOtherPlans) في المجموعة الجارية؛ واستدعاؤه مرتين غير ضار. الصفحة التي تشحن appmint-iap.js خاصًا بها تحتفظ به - وتتنحى النسخة المدمجة.
AppMintIAP.started دالة مساعدة (helper)#
window.AppMintIAP.started
مثال
خاصية (وليست دالة): true بمجرد أن يعمل المساعد، أي بعد ربط منتج واحد على الأقل من أزرار start() أو sell() أو data-iap-buy.
يعيد: خاصية منطقية. يتطلب: 💰 بيع منتجات داخل التطبيق.
استخدمه لتقرر بين start() (بخياراتك الخاصة) وsell() (الذي يعمل أيضًا على مساعد جارٍ):
function setUpShop(iap) {
if (!iap.started) {
iap.start({
plans: [{ id: 'pro_unlock', button: '#buy-pro', label: 'Unlock Pro — {price}' }],
toast: function (msg) { showMessage(msg); }
});
} else {
// Already running (the page had data-iap-buy buttons): start() would do nothing now.
iap.sell('pro_unlock', '#buy-pro', { label: 'Unlock Pro — {price}' });
}
}
if (window.AppMintIAP) setUpShop(window.AppMintIAP);نبّه نفسك أثناء التطوير عندما لا يُربط أي شيء في الصفحة (خطأ إملائي في data-iap-buy، أو لم تُعرض الأزرار قط):
window.addEventListener('load', function () {
setTimeout(function () {
var iap = window.AppMintIAP;
if (iap && !iap.started) {
console.warn('AppMintIAP is still waiting: no data-iap-buy button, sell() or start() on this page');
}
}, 3000);
});ميّز شاشة المتجر عن "لا يزال قيد التحميل" و"المشتريات غير متاحة":
function storeStatus() {
var iap = window.AppMintIAP;
if (!iap) return 'web'; // opened in a normal browser
if (!iap.started) return 'waiting'; // no product wired yet
return iap.state().available ? 'ready' : 'unavailable';
}
document.getElementById('store').setAttribute('data-status', storeStatus());ملاحظات: لا يبدأ start() ثانٍ المساعد مرتين (يشغّل React StrictMode التأثيرات مرتين)؛ وتُدمج خياراته في المجموعة الجارية. يظل false ما دام لا يوجد منتج مربوط؛ ويواصل المساعد حينها مراقبة الصفحة بحثًا عن أزرار data-iap-buy.
AppMintIAP.state دالة مساعدة (helper)#
window.AppMintIAP.state()
مثال
يعيد حالة الشراء الحالية مرة واحدة، دون اشتراك.
يعيد: {available, adFree, owned, ownedIds, products}، بشكل متزامن. ownedIds مصفوفة معرّفات منتجات؛ وproducts هو {id: {price, currency, type: 'inapp'|'subs', period, owned}} للمنتجات التي سأل المساعد Google عنها. يتطلب: 💰 بيع منتجات داخل التطبيق.
function describePass() {
if (!window.AppMintIAP) return 'Available in the Android app';
var s = window.AppMintIAP.state();
if (s.ownedIds.indexOf('season_pass') !== -1) return 'Your season pass is active';
var p = s.products.season_pass; // undefined until Google answers
if (!p || !p.price) return 'Season pass';
var every = { P1W: 'week', P1M: 'month', P1Y: 'year' }[p.period] || 'period';
return 'Season pass — ' + p.price + ' / ' + every;
}
document.getElementById('pass-label').textContent = describePass();تحقق من الملكية لحظة نقر اللاعب على مستوى مقفل، وإلا وجّهه إلى متجرك:
function openLevel(n) {
var s = window.AppMintIAP ? window.AppMintIAP.state() : null;
var needsPack = n > 10;
if (needsPack && !(s && s.ownedIds.indexOf('level_pack_2') !== -1)) {
showShop('level_pack_2'); // your own screen
return;
}
startLevel(n);
}أخفِ مطالبات "بدون إعلانات" الخاصة بك بمجرد تفعيل استحقاق الإنشاء بلا إعلانات، واسرد الأسعار التي أرسلها Google حتى الآن:
if (window.AppMintIAP) {
var s = window.AppMintIAP.state();
document.querySelectorAll('.go-ad-free').forEach(function (el) { el.hidden = s.adFree; });
var list = document.getElementById('price-list');
Object.keys(s.products).forEach(function (id) {
var p = s.products[id];
var li = document.createElement('li');
li.textContent = id + ': ' + (p.owned ? 'owned' : (p.price || '…')) +
(p.type === 'subs' ? ' (subscription)' : '');
list.appendChild(li);
});
}ملاحظات: هو لقطة. لمتابعة التغييرات استخدم AppMintIAP.onChange(fn) الذي يعطي الكائن نفسه في كل تغيير. owned هو true عندما يكون أي شيء مملوكًا؛ وتحقق من منتج بـ ownedIds أو AppMintIAP.isOwned(id).
getOwnedProducts الجسر (bridge)#
window.WebToApk.getOwnedProducts(): String
مصفوفة JSON بكل معرّف منتج مملوك - لحجب شاشات كاملة عند بدء التشغيل.
مثال
يسرد كل منتج Play يملكه المستخدم، لفتح شاشات كاملة عند البدء.
يعيد: سلسلة JSON (مصفوفة معرّفات منتجات)، بشكل متزامن، مثل '["pro_unlock","level_pack_2"]'. حلّلها بـ JSON.parse. و'[]' عندما لا يوجد شيء مملوك. يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء.
function ownedIds() {
var b = window.WebToApk;
if (!b || typeof b.getOwnedProducts !== 'function') return [];
try { return JSON.parse(b.getOwnedProducts()); } catch (e) { return []; }
}
function unlockAll(ids) {
ids.forEach(function (id) {
var el = document.querySelector('[data-product="' + id + '"]');
if (el) el.classList.add('unlocked');
});
}
unlockAll(ownedIds());
// The owned list changed (launch restore, a new purchase, a refund): the full new list.
window.addEventListener('appmint:owned-changed', function (e) {
document.querySelectorAll('.unlocked').forEach(function (el) { el.classList.remove('unlocked'); });
unlockAll(e.detail.products); // e.detail = {products: ['pro_unlock', ...]}
});التغيير نفسه يستدعي أيضًا window.onAppMintOwnedChanged(detail) إذا عرّفته.
قائمة "مشترياتي" في شاشة إعداداتك، باسمك الخاص لكل منتج:
var NAMES = { pro_unlock: 'Pro features', level_pack_2: 'Levels 11-20', remove_ads: 'No ads' };
function renderMyPurchases() {
var b = window.WebToApk, ids = [];
if (b && typeof b.getOwnedProducts === 'function') {
try { ids = JSON.parse(b.getOwnedProducts()); } catch (e) {}
}
var list = document.getElementById('my-purchases');
list.textContent = '';
ids.forEach(function (id) {
var li = document.createElement('li');
li.textContent = NAMES[id] || id;
list.appendChild(li);
});
document.getElementById('nothing-bought').hidden = ids.length > 0;
}
renderMyPurchases();
window.addEventListener('appmint:owned-changed', renderMyPurchases);احسب أفضل مستوى يملكه المستخدم عندما يكون كل مستوى منتجًا مستقلًا:
var TIERS = ['premium_plus', 'premium', 'basic']; // best first
function currentTier() {
var b = window.WebToApk, owned = [];
if (b && typeof b.getOwnedProducts === 'function') {
try { owned = JSON.parse(b.getOwnedProducts()); } catch (e) {}
}
for (var i = 0; i < TIERS.length; i++) if (owned.indexOf(TIERS[i]) !== -1) return TIERS[i];
return 'free';
}
applyTier(currentTier());
window.addEventListener('appmint:purchase', function () { applyTier(currentTier()); });
window.addEventListener('appmint:owned-changed', function () { applyTier(currentTier()); });ملاحظات: يشمل المنتجات لمرة واحدة والاشتراكات النشطة، وكذلك منتج إزالة الإعلانات إذا اشتراه المستخدم. يعمل دون اتصال؛ وتُحفظ القائمة على الهاتف عند كل استعادة تشغيل وشراء. يُطلق appmint:owned-changed فقط عندما تتغير القائمة فعلًا، فقد لا يُطلق إطلاقًا في تشغيل عادي.
getProducts الجسر (bridge)#
window.WebToApk.getProducts(requestId: String, productIdsJson: String)
يسأل Play عن التفاصيل الحية لمعرّفات المنتجات المعطاة (مصفوفة JSON من السلاسل). تصل النتيجة كـ 'appmint:products' {requestId, products:[{productId, type, title, description, price, currency, period?, owned}]}. المعرّفات التي لا يعرفها Play تغيب عن النتيجة - وتخفي الصفحة تلك العناصر.
مثال
يسأل Google Play عن التفاصيل الحية لمنتجاتك: العنوان والوصف والسعر المحلي.
يعيد: لا شيء الآن. تصل الإجابة كحدث appmint:products: {requestId, products: [{productId, type, title, description, price, currency, period?, owned}]}. type هو 'inapp' أو 'subs'؛ وperiod فترة بصيغة ISO-8601 مثل 'P1M' (للاشتراكات فقط). يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء. الوسيط الثاني سلسلة JSON: استخدم JSON.stringify.
function loadPrices(ids) {
return new Promise(function (resolve) {
var b = window.WebToApk;
if (!b || typeof b.getProducts !== 'function') { resolve([]); return; } // browser
var requestId = 'prices-' + Date.now();
window.addEventListener('appmint:products', function handler(e) {
if (String(e.detail.requestId) !== requestId) return; // another call's answer
window.removeEventListener('appmint:products', handler);
resolve(e.detail.products || []);
});
b.getProducts(requestId, JSON.stringify(ids));
});
}
loadPrices(['pro_unlock', 'coins_500', 'pro_monthly']).then(function (products) {
products.forEach(function (p) {
var btn = document.querySelector('[data-product="' + p.productId + '"]');
if (!btn) return;
btn.hidden = false;
btn.textContent = p.owned ? 'Owned' : p.title + ' · ' + p.price + (p.period === 'P1M' ? ' / month' : '');
});
});تُمرَّر الإجابة أيضًا إلى window.onAppMintProducts(detail) إذا عرّفته.
منتقي خطط اشتراك، وكل بطاقة موسومة بسعر Play وفترة الفوترة:
var PERIODS = { P1W: 'week', P1M: 'month', P3M: '3 months', P6M: '6 months', P1Y: 'year' };
window.addEventListener('appmint:products', function (e) {
if (e.detail.requestId !== 'plans') return;
e.detail.products.forEach(function (p) {
var card = document.querySelector('.plan[data-product="' + p.productId + '"]');
if (!card || p.type !== 'subs') return;
card.hidden = false;
card.querySelector('.price').textContent = p.price + ' / ' + (PERIODS[p.period] || p.period);
card.classList.toggle('current', p.owned);
});
});
var b = window.WebToApk;
if (b && typeof b.getProducts === 'function') {
b.getProducts('plans', JSON.stringify(['pro_weekly', 'pro_monthly', 'pro_yearly']));
}أثناء الاختبار، سجّل المعرّفات التي لم يُعدها Google Play، عبر خطّاف الصفحة:
var WANTED = ['pro_unlock', 'coins_500', 'pro_monthly'];
window.onAppMintProducts = function (detail) { // same payload as appmint:products
if (detail.requestId !== 'store-check') return;
var found = detail.products.map(function (p) { return p.productId; });
var missing = WANTED.filter(function (id) { return found.indexOf(id) === -1; });
if (missing.length) console.warn('Not returned by Google Play: ' + missing.join(', '));
document.getElementById('store').hidden = found.length === 0;
};
if (window.WebToApk && typeof window.WebToApk.getProducts === 'function') {
window.WebToApk.getProducts('store-check', JSON.stringify(WANTED));
}ملاحظات: اعرض price تمامًا كما يعطيه Play ولا تخزّنه أبدًا. المعرّف الذي لا يعرفه Play (غير مُنشأ، غير نشط، أو لم يُثبَّت التطبيق من Play) يغيب ببساطة عن products؛ أبقِ ذلك العنصر مخفيًا. قيمة requestId التي تبدو رقمية تعود كرقم، فاستخدم معرّفًا نصيًا أو قارن بـ String().
isOwned الجسر (bridge)#
window.WebToApk.isOwned(productId: String): Boolean
فحص فوري على الجهاز للملكية (منتج لمرة واحدة مملوك أو اشتراك نشط).
مثال
يتحقق فورًا ودون اتصال مما إذا كان المستخدم يملك أحد منتجات Play الخاصة بك (منتج لمرة واحدة، أو اشتراك لا يزال نشطًا).
يعيد: true / false، بشكل متزامن. يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء. مع تفعيل هذا المفتاح يجيب أيضًا عن منتج إزالة الإعلانات (IAP)؛ وفي تطبيق يبيع إزالة الإعلانات فقط (المفتاح معطّل) يجيب بـ false لكل منتج، فتحقق من إزالة الإعلانات بـ isPremium() هناك.
function hasPro() {
var b = window.WebToApk;
return !!(b && typeof b.isOwned === 'function' && b.isOwned('pro_unlock'));
}
function paint() {
document.getElementById('pro-features').hidden = !hasPro();
document.getElementById('buy-pro').hidden = hasPro();
}
paint();
// Ownership changes: a purchase landed, or the launch restore / a refund changed the list.
window.addEventListener('appmint:purchase', paint);
window.addEventListener('appmint:owned-changed', paint);تحقق لحظة الاستخدام: تصدير Pro فقط يعمل إذا كان مملوكًا، وإلا يفتح نافذة الشراء ويكتمل بعد إتمام الشراء:
var exportWaiting = false;
document.getElementById('export-hd').addEventListener('click', function () {
var b = window.WebToApk;
if (!b || typeof b.isOwned !== 'function') { exportStandard(); return; } // browser
if (b.isOwned('pro_unlock')) { exportHd(); return; }
exportWaiting = true;
b.purchase('pro_unlock');
});
window.addEventListener('appmint:purchase', function (e) {
if (e.detail.productId === 'pro_unlock' && exportWaiting) {
exportWaiting = false;
exportHd(); // finish what the user asked for
}
});Pro عبر أي من عدة خطط: منتج شهري وسنوي ومدى الحياة تُحتسب جميعها:
var PRO_PLANS = ['pro_monthly', 'pro_yearly', 'pro_lifetime'];
function isPro() {
var b = window.WebToApk;
if (!b || typeof b.isOwned !== 'function') return false;
return PRO_PLANS.some(function (id) { return b.isOwned(id); });
}
function paintPlan() { document.body.classList.toggle('pro', isPro()); }
paintPlan();
window.addEventListener('appmint:purchase', paintPlan);
window.addEventListener('appmint:owned-changed', paintPlan); // an expired subscription or a refundملاحظات: تُستعاد قائمة المملوكات من حساب Google للمستخدم عند كل تشغيل وتُحفظ على الهاتف، فتعمل دون اتصال وبعد إعادة التثبيت، دون خادم. تكون false دائمًا في المتصفح. لا يُحسب الاشتراك بعد أن يتوقف Play عن الإبلاغ بأنه نشط.
isPremium الجسر (bridge)#
window.WebToApk.isPremium(): Boolean
صحيح عندما يملك المستخدم "إزالة الإعلانات" (استحقاق Play Billing، يُستعاد تلقائيًا عند كل تشغيل). يطابق السبب 'ad_free' في 'appmint:ad-unavailable'.
مثال
يخبرك ما إذا كان المستخدم يملك منتج "إزالة الإعلانات"، ليخفي الصفحة زر "إزالة الإعلانات" وواجهة الإعلانات.
يعيد: true / false، بشكل متزامن. يتطلب: الإعلانات مفعّلة وإزالة الإعلانات (IAP) مع معرّف منتج Play Store. بدون ذلك تكون دائمًا false.
var removeAdsBtn = document.getElementById('remove-ads');
function paintPremium(premium) {
removeAdsBtn.hidden = premium;
document.body.classList.toggle('premium', premium);
}
var b = window.WebToApk;
paintPremium(!!(b && typeof b.isPremium === 'function' && b.isPremium()));
// Changes later: purchase finished, restore at launch, refund or expired subscription.
window.addEventListener('appmint:premium', function (e) {
paintPremium(e.detail.premium); // e.detail = {premium: true | false}
});التغيير نفسه يستدعي أيضًا خطّاف الصفحة، إن عرّفته:
window.onAppMintPremiumChanged = function (info) { paintPremium(info.premium); }; // {premium}اعرض عرض "إزالة الإعلانات" الخاص بك في شاشة نهاية اللعبة فقط للاعبين الذين ما زالوا يرون الإعلانات:
function showGameOver(score) {
document.getElementById('final-score').textContent = score;
var b = window.WebToApk;
var premium = !!(b && typeof b.isPremium === 'function' && b.isPremium());
// Ad-free already, or a plain browser (no ads there): no offer.
document.getElementById('remove-ads-offer').hidden = premium || !b;
document.getElementById('game-over').hidden = false;
}ملاحظات: تُستعاد الملكية من حساب Google للمستخدم عند كل تشغيل، فاقرأها مرة عند البدء ثم تابع appmint:premium. يقبل حقل معرّف المنتج عدة معرّفات مفصولة بفواصل (مثل خطة شهرية وأخرى مدى الحياة)؛ ويكفي امتلاك أي منها. أثناء الاشتراك المميز تُزال الإعلانات وتجيب استدعاءات الإعلانات ad_free.
purchase الجسر (bridge)#
window.WebToApk.purchase(productId: String)
يفتح نافذة شراء Google لـ [productId] (منتج لمرة واحدة أو اشتراك - يُحدَّد تلقائيًا). امنح فقط عند 'appmint:purchase' {productId, owned:true}؛ وتطلق الإخفاقات 'appmint:purchase-failed' {reason}. المنتج المملوك بالفعل يعيد إطلاق حدث الشراء لتفتح الصفحة بشكل آمن للتكرار.
مثال
يفتح نافذة شراء Google Play لأحد منتجاتك (منتج لمرة واحدة أو اشتراك؛ ويعرف التطبيق أيهما).
يعيد: لا شيء الآن. النجاح يطلق appmint:purchase {productId, owned: true}. والفشل يطلق appmint:purchase-failed {productId, reason}. يتطلب: 💰 بيع منتجات داخل التطبيق في معالج الإنشاء، وأن يكون المنتج مُنشأً ونشطًا في Play Console.
افتح القفل فقط في معالج appmint:purchase، وليس عند النقر أبدًا:
var b = window.WebToApk;
var buyBtn = document.getElementById('buy-pro');
if (!b || typeof b.purchase !== 'function') {
buyBtn.hidden = true; // browser: nothing to buy here
} else {
buyBtn.addEventListener('click', function () {
buyBtn.disabled = true;
b.purchase('pro_unlock');
});
}
window.addEventListener('appmint:purchase', function (e) {
if (e.detail.productId === 'pro_unlock') unlockPro(); // safe to run twice
buyBtn.disabled = false;
});
window.addEventListener('appmint:purchase-failed', function (e) {
buyBtn.disabled = false;
if (e.detail.reason === 'pending') showMessage('Payment pending. It unlocks by itself once Google confirms.');
else showMessage('Purchase is not available right now (' + e.detail.reason + ').');
});
// A cancel on Google's sheet sends no event: release the button when the page is visible again.
document.addEventListener('visibilitychange', function () { if (!document.hidden) buyBtn.disabled = false; });النتائج نفسها تستدعي أيضًا window.onAppMintPurchase(detail) وwindow.onAppMintPurchaseFailed(detail) إذا عرّفتهما.
اشترِ الخطة التي اختارها المستخدم في منتقي الاشتراكات، وأخفِ خطة لا يعرف Play معرّفها:
var picked = 'pro_yearly';
document.querySelectorAll('.plan').forEach(function (card) {
card.addEventListener('click', function () { picked = card.getAttribute('data-product'); });
});
document.getElementById('subscribe').addEventListener('click', function () {
var b = window.WebToApk;
if (b && typeof b.purchase === 'function') b.purchase(picked);
});
window.addEventListener('appmint:purchase', function (e) {
if (/^pro_/.test(e.detail.productId)) unlockPro(); // any plan unlocks Pro
});
window.addEventListener('appmint:purchase-failed', function (e) {
if (e.detail.reason !== 'not_found') return;
var card = document.querySelector('.plan[data-product="' + e.detail.productId + '"]');
if (card) card.hidden = true;
});افتح حزمة مستويات من المستوى المقفل الذي نقر عليه اللاعب، ثم افتح ذلك المستوى:
var wantedLevel = null;
function openLevel(n) {
var b = window.WebToApk;
var owned = !!(b && typeof b.isOwned === 'function' && b.isOwned('level_pack_2'));
if (n <= 10 || owned) { startLevel(n); return; }
if (!b || typeof b.purchase !== 'function') { showMessage('Levels 11-20 are in the Android app.'); return; }
wantedLevel = n;
b.purchase('level_pack_2');
}
window.onAppMintPurchase = function (detail) { // {productId, owned: true}
if (detail.productId !== 'level_pack_2' || !wantedLevel) return;
var n = wantedLevel;
wantedLevel = null;
startLevel(n);
};ملاحظات: الأسباب: disabled (بيع المنتجات داخل التطبيق معطّل)، not_found (المعرّف غير موجود في Play Console)، billing_error، pending (دفع المستخدم بطريقة بطيئة؛ يصل الفتح لاحقًا عبر الاستعادة عند التشغيل وappmint:owned-changed). إذا كان المنتج مملوكًا بالفعل، يُطلق appmint:purchase مجددًا فورًا، لذا يجب أن يكون فتحك آمنًا للتكرار. يعرض التطبيق أيضًا رسالته القصيرة للأخطاء.
revenuecat الجسر (bridge)#
window.WebToApk.revenuecat(action: String, argsJson: String, callbackId: String)
إجراء [action] واحد من RevenueCat مع [argsJson]؛ ويعيد [callbackId] مع {ok, …} أو {ok:false, error}.
مثال
الطريقة الخام خلف AppMint.revenuecat - استخدم الغلاف. نقطة دخول واحدة: action هو configure أو getOfferings أو purchase أو restore أو getCustomerInfo أو logIn أو logOut أو getAppUserId.
يعيد: لا شيء مباشرة؛ ويحل window.__appmintReplies[callbackId] بالإجابة نفسها التي يعطيها الغلاف. يتطلب: خطوة التكامل ← RevenueCat بمفتاح goog_ العام الخاص بك.
window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc1'] = (r) => console.log(r.ok ? r.customerInfo : r.error);
WebToApk.revenuecat('getCustomerInfo', '{}', 'rc1');اضبط بمعرّف مستخدم واقرأ العروض:
window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc-cfg'] = (r) => {
if (!r.ok) return console.warn('RevenueCat:', r.error);
window.__appmintReplies['rc-off'] = (o) => console.log(o.ok ? o.current : o.error);
WebToApk.revenuecat('getOfferings', '{}', 'rc-off');
};
if (window.WebToApk && WebToApk.revenuecat) {
WebToApk.revenuecat('configure', JSON.stringify({ appUserId: 'u_42' }), 'rc-cfg');
}اشترِ وعالج حالات الرفض:
window.__appmintReplies = window.__appmintReplies || {};
window.__appmintReplies['rc-buy'] = (r) => {
if (r.ok) return unlock(r.customerInfo.entitlements.active);
if (r.error === 'foreign_origin') return showMessage('Purchases only work inside the app.');
if (r.cancelled) return;
showMessage('Purchase failed: ' + (r.message || r.error));
};
WebToApk.revenuecat('purchase', JSON.stringify({ package: '$rc_monthly' }), 'rc-buy');ملاحظات: انظر AppMint.revenuecat لوسائط كل إجراء وإجاباته.
startRemoveAdsPurchase الجسر (bridge)#
window.WebToApk.startRemoveAdsPurchase()
أطلق مسار شراء "إزالة الإعلانات" في Google Play من الصفحة (بمبادرة المستخدم، مثل زر "إزالة الإعلانات" في تطبيقك). بدون هذا كانت نقطة الدخول الوحيدة عنصر القائمة الجانبية الأصلي - وهو غير متاح في التطبيقات المبنية بلا قائمة جانبية. تصل النتيجة إلى الصفحة عبر 'appmint:premium' {premium:true}؛ وإذا كان مملوكًا بالفعل يُعاد الإشعار فورًا؛ وتظهر أخطاء المسار كرسالة منبثقة أصلية.
مثال
يفتح نافذة شراء Google Play لمنتج "إزالة الإعلانات" الخاص بك، من زر "إزالة الإعلانات" في صفحتك.
يعيد: لا شيء. تصل النتيجة كـ appmint:premium {premium: true} (وكذلك window.onAppMintPremiumChanged). يتطلب: الإعلانات مفعّلة وإزالة الإعلانات (IAP) مع معرّف منتج Play Store (أول معرّف في القائمة هو المباع). عند تفعيل القائمة الجانبية يكون للتطبيق أيضًا عنصر إزالة الإعلانات الخاص به هناك (يختفي بمجرد امتلاك المستخدم له)؛ ومع إيقاف القائمة الجانبية يكون هذا الزر في صفحتك هو الطريقة الوحيدة للشراء.
var b = window.WebToApk;
var btn = document.getElementById('remove-ads');
if (!b || typeof b.startRemoveAdsPurchase !== 'function' || b.isPremium()) {
btn.hidden = true; // browser, or already ad-free
} else {
btn.addEventListener('click', function () {
btn.disabled = true;
b.startRemoveAdsPurchase();
});
}
window.addEventListener('appmint:premium', function (e) {
if (e.detail.premium) { btn.hidden = true; showMessage('Thank you! Ads are gone.'); }
});
// A cancel on Google's sheet sends no event: release the button when the page is visible again.
document.addEventListener('visibilitychange', function () {
if (!document.hidden) btn.disabled = false;
});صف في الإعدادات يعرض ما إذا كانت الإعلانات مُزالة، مع الزر الذي يشتريها:
var row = document.getElementById('ads-setting');
var btn = row.querySelector('button');
var b = window.WebToApk;
function paintAdsRow(premium) {
row.querySelector('.status').textContent = premium ? 'Ads removed. Thank you!' : 'Ads are on';
btn.hidden = premium;
}
if (b && typeof b.startRemoveAdsPurchase === 'function') {
paintAdsRow(b.isPremium());
btn.addEventListener('click', function () { b.startRemoveAdsPurchase(); });
window.onAppMintPremiumChanged = function (info) { paintAdsRow(info.premium); }; // {premium}
} else {
row.hidden = true; // browser: nothing to sell
}اعرضه بعد كل ثلاث جولات منتهية، في مربع حوار يجيب عنه اللاعب بنقرة:
var rounds = 0;
var offer = document.getElementById('remove-ads-offer'); // a <dialog>
function onRoundFinished() {
var b = window.WebToApk;
if (!b || typeof b.startRemoveAdsPurchase !== 'function' || b.isPremium()) return;
rounds += 1;
if (rounds % 3 === 0) offer.showModal();
}
offer.querySelector('.buy').addEventListener('click', function () {
offer.close();
window.WebToApk.startRemoveAdsPurchase(); // opened from the player's own tap
});
offer.querySelector('.later').addEventListener('click', function () { offer.close(); });ملاحظات: إذا كان المستخدم يملكه بالفعل، يُطلق appmint:premium {premium:true} فورًا. يعرض التطبيق الأخطاء (تعذر الوصول إلى Play، المنتج غير موجود في Play Console) كرسالة أصلية قصيرة؛ ولا تصل الصفحة أي حدث بشأنها. اختبر بتطبيق مثبّت من مسار اختبار في Play.