Shop: branch OPEN_CHECKOUT by payment rail + add license-request handler
This commit is contained in:
@@ -436,6 +436,24 @@ int32 BattlePayMgr::GetCharacterBoostType()
|
|||||||
return int32(sWorld->getIntConfig(CONFIG_SHOP_CHARACTER_BOOST_TYPE));
|
return int32(sWorld->getIntConfig(CONFIG_SHOP_CHARACTER_BOOST_TYPE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShopRealMoneyMode BattlePayMgr::GetRealMoneyMode()
|
||||||
|
{
|
||||||
|
// Read the string directly from the config: World has no string-config array, and this is queried
|
||||||
|
// only on OPEN_CHECKOUT of a real-money product (rare), so a per-call lookup is fine. Default
|
||||||
|
// "instant" so a fresh test realm can buy real-money products for QA without a web backend.
|
||||||
|
std::string const mode = sConfigMgr->GetStringDefault("Shop.RealMoney.Mode", "instant");
|
||||||
|
if (mode == "web")
|
||||||
|
return SHOP_REAL_MONEY_WEB;
|
||||||
|
if (mode == "disabled")
|
||||||
|
return SHOP_REAL_MONEY_DISABLED;
|
||||||
|
if (mode == "instant")
|
||||||
|
return SHOP_REAL_MONEY_INSTANT;
|
||||||
|
|
||||||
|
TC_LOG_ERROR("server.loading", "BattlePay: Shop.RealMoney.Mode = '{}' is not one of web|instant|disabled; "
|
||||||
|
"using 'instant'.", mode);
|
||||||
|
return SHOP_REAL_MONEY_INSTANT;
|
||||||
|
}
|
||||||
|
|
||||||
bool BattlePayMgr::IsCharacterBoostProduct(ShopProduct const& product)
|
bool BattlePayMgr::IsCharacterBoostProduct(ShopProduct const& product)
|
||||||
{
|
{
|
||||||
return GetServiceType(product) == SHOP_SERVICE_CHARACTER_BOOST;
|
return GetServiceType(product) == SHOP_SERVICE_CHARACTER_BOOST;
|
||||||
|
|||||||
@@ -29,6 +29,29 @@
|
|||||||
|
|
||||||
class Player;
|
class Player;
|
||||||
|
|
||||||
|
// Payment method of a shop product (ShopProduct::Currency). Distinct from the client's CurrencyTypes.db2
|
||||||
|
// ids - this is how the SERVER charges. REAL_MONEY is the Rail A marker: such a product is never charged
|
||||||
|
// on the wire; its checkout goes to the shop2 web overlay (or is granted instantly for QA) per
|
||||||
|
// Shop.RealMoney.Mode. The COMMERCE_MASTER_PLAN's "CurrencyTypesID == 0 (real money)" maps to this value,
|
||||||
|
// because value 0 here already means "free".
|
||||||
|
enum ShopCurrency : uint8
|
||||||
|
{
|
||||||
|
SHOP_CURRENCY_FREE = 0, // no cost
|
||||||
|
SHOP_CURRENCY_GOLD = 1, // copper
|
||||||
|
SHOP_CURRENCY_ITEM_TOKEN = 2, // a token item (PriceItemId x PriceItemCount)
|
||||||
|
SHOP_CURRENCY_CUSTOM = 3, // custom currency amount
|
||||||
|
SHOP_CURRENCY_REAL_MONEY = 4 // real money -> Rail A (web checkout); never charged on the wire
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server behaviour when a real-money (Rail A) product's checkout is opened. Read from the
|
||||||
|
// Shop.RealMoney.Mode config string; see BattlePayMgr::GetRealMoneyMode.
|
||||||
|
enum ShopRealMoneyMode : uint8
|
||||||
|
{
|
||||||
|
SHOP_REAL_MONEY_WEB = 0, // let the client web-checkout; the realm sends no game response
|
||||||
|
SHOP_REAL_MONEY_INSTANT = 1, // grant server-side immediately with no charge (private-realm QA)
|
||||||
|
SHOP_REAL_MONEY_DISABLED = 2 // refuse: no SSO token, no grant
|
||||||
|
};
|
||||||
|
|
||||||
// A single deliverable payload of a shop product (>1 per product = a bundle).
|
// A single deliverable payload of a shop product (>1 per product = a bundle).
|
||||||
struct ShopDeliverable
|
struct ShopDeliverable
|
||||||
{
|
{
|
||||||
@@ -106,7 +129,7 @@ struct ShopProduct
|
|||||||
bool Enabled = true;
|
bool Enabled = true;
|
||||||
std::string Name;
|
std::string Name;
|
||||||
std::string Description;
|
std::string Description;
|
||||||
uint8 Currency = 1; // 0 free | 1 gold(copper) | 2 item-token | 3 custom-currency
|
uint8 Currency = 1; // ShopCurrency: 0 free | 1 gold | 2 item-token | 3 custom | 4 real-money(web)
|
||||||
uint64 Price = 0; // copper (currency 1) or currency amount (3)
|
uint64 Price = 0; // copper (currency 1) or currency amount (3)
|
||||||
uint32 PriceItemId = 0; // currency 2: token item
|
uint32 PriceItemId = 0; // currency 2: token item
|
||||||
uint32 PriceItemCount = 0;
|
uint32 PriceItemCount = 0;
|
||||||
@@ -194,6 +217,11 @@ public:
|
|||||||
// The VAS service type a product's type-5 deliverable names, or 0 if it has none.
|
// The VAS service type a product's type-5 deliverable names, or 0 if it has none.
|
||||||
static uint8 GetServiceType(ShopProduct const& product);
|
static uint8 GetServiceType(ShopProduct const& product);
|
||||||
|
|
||||||
|
// How the realm treats a real-money (Rail A) checkout, from the Shop.RealMoney.Mode config string
|
||||||
|
// ("web" | "instant" | "disabled"; unknown values fall back to the default). See the OPEN_CHECKOUT
|
||||||
|
// rail branch in HandleBattlePayOpenCheckout.
|
||||||
|
static ShopRealMoneyMode GetRealMoneyMode();
|
||||||
|
|
||||||
// Allocates the next DistributionID (realm-namespaced, seeded from the store at startup).
|
// Allocates the next DistributionID (realm-namespaced, seeded from the store at startup).
|
||||||
uint64 GenerateDistributionID() { return ++_distributionCounter; }
|
uint64 GenerateDistributionID() { return ++_distributionCounter; }
|
||||||
|
|
||||||
|
|||||||
@@ -591,11 +591,103 @@ void WorldSession::HandleBattlePayOpenCheckout(WorldPackets::BattlePay::OpenChec
|
|||||||
if (!sWorld->getBoolConfig(CONFIG_SHOP_ENABLED))
|
if (!sWorld->getBoolConfig(CONFIG_SHOP_ENABLED))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Retail answers CMSG_BATTLE_PAY_OPEN_CHECKOUT with SMSG_GENERATE_SSO_TOKEN_RESPONSE as a strict
|
// Two payment rails, decided by the checked-out product (resolve the advertised productID -> catalog):
|
||||||
// 1:1 echo of the request's ClientToken (proven in all 8 captures: checkout #N -> response #N with
|
//
|
||||||
// the same u32). Answering from here - rather than pushing the token unsolicited at login - is what
|
// Rail B - IN-GAME CURRENCY. A product we route whose Currency is anything but real money is bought on
|
||||||
// lets checkouts #2+ get a reply. See COMMERCE_AUDIT C-09 / WOW_TOKEN_RE_68275.md.
|
// the wire. Keep the proven handshake: SMSG_GENERATE_SSO_TOKEN_RESPONSE echoing the request's
|
||||||
|
// ClientToken 1:1 (COMMERCE_AUDIT C-09 / WOW_TOKEN_RE_68275.md). Answering from here - not from an
|
||||||
|
// unsolicited push at login - is what lets checkouts #2+ get a reply.
|
||||||
|
//
|
||||||
|
// Rail A - REAL MONEY. Either a product explicitly flagged Currency 4 (real money), OR an un-reskinned
|
||||||
|
// retail catalog card we do not route to a shop_product (e.g. the Midnight expansion, ProductID
|
||||||
|
// 0x417070). The 12.1.0.69382 sniff proves the client gets NO game-packet response for such a
|
||||||
|
// product and opens the shop2 web overlay itself; the old 1:1 SSO echo here was only ever correct for
|
||||||
|
// in-game-currency items. Behaviour is governed by Shop.RealMoney.Mode.
|
||||||
|
ShopProduct const* product = sBattlePayMgr->GetProductByAdvertisedId(openCheckout.ProductID);
|
||||||
|
if (product && product->Currency != SHOP_CURRENCY_REAL_MONEY)
|
||||||
|
{
|
||||||
SendGenerateSsoToken(openCheckout.ClientToken);
|
SendGenerateSsoToken(openCheckout.ClientToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (BattlePayMgr::GetRealMoneyMode())
|
||||||
|
{
|
||||||
|
case SHOP_REAL_MONEY_INSTANT:
|
||||||
|
{
|
||||||
|
// Grant the product server-side with no charge (private-realm QA default). Reuse the proven
|
||||||
|
// delivery path: a real-money product carries Currency 4, which the charge checks (== gold,
|
||||||
|
// == item token) never match, so nothing is deducted. An unrouted retail card has no
|
||||||
|
// shop_product to grant - honest no-op rather than a faked success.
|
||||||
|
if (!product)
|
||||||
|
{
|
||||||
|
TC_LOG_INFO("network", "BattlePay: {} opened a real-money checkout for unrouted product {} with "
|
||||||
|
"Shop.RealMoney.Mode=instant, but no shop_product is seeded for it - nothing to grant.",
|
||||||
|
GetPlayerInfo(), openCheckout.ProductID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client resends OPEN_CHECKOUT on each web-payment failure (7 retries in the capture);
|
||||||
|
// throttle identical rapid retries so instant mode grants exactly once, reusing the same
|
||||||
|
// per-session guard as the StartPurchase path.
|
||||||
|
static constexpr uint32 BATTLEPAY_INSTANT_THROTTLE_MS = 2000;
|
||||||
|
uint32 const now = getMSTime();
|
||||||
|
if (_battlePayPurchaseInFlight ||
|
||||||
|
(_lastBattlePayPurchaseMSTime && getMSTimeDiff(_lastBattlePayPurchaseMSTime, now) < BATTLEPAY_INSTANT_THROTTLE_MS))
|
||||||
|
{
|
||||||
|
TC_LOG_DEBUG("network", "BattlePay: throttled duplicate real-money instant checkout from {} (product {}).",
|
||||||
|
GetPlayerInfo(), openCheckout.ProductID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastBattlePayPurchaseMSTime = now;
|
||||||
|
|
||||||
|
TC_LOG_INFO("network", "BattlePay: {} real-money checkout for product {} granted instantly "
|
||||||
|
"(Shop.RealMoney.Mode=instant, no charge).", GetPlayerInfo(), openCheckout.ProductID);
|
||||||
|
|
||||||
|
_battlePayPurchaseInFlight = true;
|
||||||
|
BattlePayProcessPurchase(openCheckout.ProductID);
|
||||||
|
_battlePayPurchaseInFlight = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case SHOP_REAL_MONEY_DISABLED:
|
||||||
|
// Refuse politely: no SSO token, no grant. The client opened its own web overlay on
|
||||||
|
// OPEN_CHECKOUT and the realm cannot recall that, but it neither authenticates the web session
|
||||||
|
// nor delivers anything, so the purchase cannot complete. Logged for the operator.
|
||||||
|
TC_LOG_INFO("network", "BattlePay: {} attempted a real-money checkout for product {} but "
|
||||||
|
"Shop.RealMoney.Mode=disabled - refused (no SSO, no grant).", GetPlayerInfo(), openCheckout.ProductID);
|
||||||
|
return;
|
||||||
|
case SHOP_REAL_MONEY_WEB:
|
||||||
|
default:
|
||||||
|
// Sniff-accurate: the client already opened the shop2 HTTPS overlay from OPEN_CHECKOUT, so the
|
||||||
|
// game connection sends NO response. Completion, if any, arrives from the web backend (Rail A3).
|
||||||
|
TC_LOG_DEBUG("network", "BattlePay: {} opened a real-money web checkout for product {} (client token {}); "
|
||||||
|
"no game response (Shop.RealMoney.Mode=web).", GetPlayerInfo(), openCheckout.ProductID, openCheckout.ClientToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. The client sends this mid-checkout carrying its own
|
||||||
|
// license / game data (variable body: 876/140/52/36/24 bytes in the 12.1.0.69382 capture). Retail answers
|
||||||
|
// on SMSG opcode 0x4202C1 - which our opcode map currently mis-names SMSG_VAS_GET_QUEUE_MINUTES_RESPONSE
|
||||||
|
// (the sniff's 0x4502C1 is that opcode plus the sniff's fixed 0x030000 server-opcode bias) - with an
|
||||||
|
// 11465/1025/423/403/40-byte payload.
|
||||||
|
//
|
||||||
|
// That response is a nested reflection bitstream whose FIELD LAYOUT is not recovered - the capture gives
|
||||||
|
// only the sizes, not the member types. Emitting a guessed layout on that opcode would risk the client's
|
||||||
|
// reflection parser rejecting it and disconnecting, which is exactly the "do not invent wire" hazard. So
|
||||||
|
// this handler does the honest, safe thing: it makes the opcode an EXPECTED, real handler (no more
|
||||||
|
// STATUS_UNHANDLED / Handle_NULL), logs the body size so a future capture can be matched to a variant, and
|
||||||
|
// sends nothing. The 69382 capture shows the checkout proceeds (the client drives its own web overlay from
|
||||||
|
// OPEN_CHECKOUT) without the realm answering this, so withholding the un-modeled response does not stall
|
||||||
|
// the client. When the response wire is recovered, this is where it is sent.
|
||||||
|
void WorldSession::HandleCatalogShopLicenseGameDataRequest(WorldPackets::BattlePay::CatalogShopLicenseGameDataRequest& request)
|
||||||
|
{
|
||||||
|
if (!sWorld->getBoolConfig(CONFIG_SHOP_ENABLED))
|
||||||
|
return;
|
||||||
|
|
||||||
|
TC_LOG_INFO("network", "BattlePay: CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST from {} ({} body bytes). The paired "
|
||||||
|
"response (SMSG 0x4202C1) is an un-modeled reflection payload and is deliberately not answered - see the "
|
||||||
|
"handler note. If the client stalls waiting for it, capture the response bytes and model them here.",
|
||||||
|
GetPlayerInfo(), request.Data.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
|||||||
@@ -178,6 +178,18 @@ namespace WorldPackets::BattlePay
|
|||||||
void OpenCheckout::Read()
|
void OpenCheckout::Read()
|
||||||
{
|
{
|
||||||
_worldPacket >> ClientToken;
|
_worldPacket >> ClientToken;
|
||||||
|
_worldPacket >> ProductID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CatalogShopLicenseGameDataRequest::Read()
|
||||||
|
{
|
||||||
|
// The response wire is not yet modeled, so this body is retained for diagnostics only (its size
|
||||||
|
// identifies which of the captured request variants it is). Consume the whole body so the packet
|
||||||
|
// is not reported as under-read.
|
||||||
|
size_t const len = _worldPacket.size();
|
||||||
|
Data.resize(len);
|
||||||
|
if (len)
|
||||||
|
_worldPacket.read(Data.data(), len);
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldPacket const* StartPurchaseResponse::Write()
|
WorldPacket const* StartPurchaseResponse::Write()
|
||||||
|
|||||||
@@ -47,6 +47,23 @@ namespace WorldPackets
|
|||||||
void Read() override {}
|
void Read() override {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. Sent by the client mid-checkout carrying its own
|
||||||
|
// license / game data. The body is variable (876/140/52/36/24 bytes across the 12.1.0.69382
|
||||||
|
// capture). We keep the whole body so the handler can log its size for future response modeling;
|
||||||
|
// the paired response wire (SMSG 0x4202C1) is not yet recovered - see
|
||||||
|
// WorldSession::HandleCatalogShopLicenseGameDataRequest.
|
||||||
|
class CatalogShopLicenseGameDataRequest final : public ClientPacket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit CatalogShopLicenseGameDataRequest(WorldPacket&& packet)
|
||||||
|
: ClientPacket(CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST, std::move(packet)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Read() override;
|
||||||
|
|
||||||
|
std::vector<uint8> Data; // raw request body, retained for diagnostics only
|
||||||
|
};
|
||||||
|
|
||||||
// The 12.0.7 catalog is a nested reflection bitstream that cannot be re-serialized field-by-field
|
// The 12.0.7 catalog is a nested reflection bitstream that cannot be re-serialized field-by-field
|
||||||
// offline (see docs). For P0 we replay a byte-exact, client-validated catalog blob captured from a
|
// offline (see docs). For P0 we replay a byte-exact, client-validated catalog blob captured from a
|
||||||
// real 68275 session, so the shop opens and displays real products. RawData is the message BODY
|
// real 68275 session, so the shop opens and displays real products. RawData is the message BODY
|
||||||
@@ -403,9 +420,10 @@ namespace WorldPackets
|
|||||||
bool Flag = false;
|
bool Flag = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Client opens the checkout. The u32 is the ClientToken the server must echo back verbatim in
|
// Client opens the checkout. Body is 8 bytes: { u32 ClientToken, u32 ProductID } (12.1.0.69382
|
||||||
// SMSG_GENERATE_SSO_TOKEN_RESPONSE (proven 1:1 in all 8 captures - checkout #N -> response #N
|
// capture, Midnight ProductID 0x417070 in all 7 attempts). ClientToken is the token the SSO/token
|
||||||
// with the same u32). It is not a distributionID. See COMMERCE_AUDIT C-09.
|
// handshake echoes back verbatim for an in-game-currency product (COMMERCE_AUDIT C-09); ProductID
|
||||||
|
// selects the product and decides the payment rail - see HandleBattlePayOpenCheckout.
|
||||||
class OpenCheckout final : public ClientPacket
|
class OpenCheckout final : public ClientPacket
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -414,6 +432,7 @@ namespace WorldPackets
|
|||||||
void Read() override;
|
void Read() override;
|
||||||
|
|
||||||
uint32 ClientToken = 0;
|
uint32 ClientToken = 0;
|
||||||
|
uint32 ProductID = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Server-driven purchase confirmation prompt (retail interposes this between StartPurchase and
|
// Server-driven purchase confirmation prompt (retail interposes this between StartPurchase and
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ void OpcodeTable::InitializeClientOpcodes()
|
|||||||
DEFINE_HANDLER(CMSG_CAN_DUEL, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleCanDuel);
|
DEFINE_HANDLER(CMSG_CAN_DUEL, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleCanDuel);
|
||||||
DEFINE_HANDLER(CMSG_CAN_REDEEM_TOKEN_FOR_BALANCE, STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleCanRedeemTokenForBalance);
|
DEFINE_HANDLER(CMSG_CAN_REDEEM_TOKEN_FOR_BALANCE, STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleCanRedeemTokenForBalance);
|
||||||
DEFINE_HANDLER(CMSG_CAST_SPELL, STATUS_LOGGEDIN, PROCESS_THREADSAFE, &WorldSession::HandleCastSpellOpcode);
|
DEFINE_HANDLER(CMSG_CAST_SPELL, STATUS_LOGGEDIN, PROCESS_THREADSAFE, &WorldSession::HandleCastSpellOpcode);
|
||||||
DEFINE_HANDLER(CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
|
DEFINE_HANDLER(CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST, STATUS_AUTHED, PROCESS_THREADUNSAFE, &WorldSession::HandleCatalogShopLicenseGameDataRequest);
|
||||||
DEFINE_HANDLER(CMSG_CHALLENGE_MODE_REQUEST_LEADERS, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleChallengeModeRequestLeaders);
|
DEFINE_HANDLER(CMSG_CHALLENGE_MODE_REQUEST_LEADERS, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleChallengeModeRequestLeaders);
|
||||||
DEFINE_HANDLER(CMSG_CHANGE_BAG_SLOT_FLAG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleChangeBagSlotFlag);
|
DEFINE_HANDLER(CMSG_CHANGE_BAG_SLOT_FLAG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleChangeBagSlotFlag);
|
||||||
DEFINE_HANDLER(CMSG_CHANGE_BANK_BAG_SLOT_FLAG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::Handle_NULL);
|
DEFINE_HANDLER(CMSG_CHANGE_BANK_BAG_SLOT_FLAG, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::Handle_NULL);
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ namespace WorldPackets
|
|||||||
class GetPurchaseList;
|
class GetPurchaseList;
|
||||||
class StartPurchase;
|
class StartPurchase;
|
||||||
class OpenCheckout;
|
class OpenCheckout;
|
||||||
|
class CatalogShopLicenseGameDataRequest;
|
||||||
class ConfirmPurchaseResponse;
|
class ConfirmPurchaseResponse;
|
||||||
class DistributionAssignToTarget;
|
class DistributionAssignToTarget;
|
||||||
class CharacterUpgradeStart;
|
class CharacterUpgradeStart;
|
||||||
@@ -2396,6 +2397,7 @@ public:
|
|||||||
void HandleBattlePayGetPurchaseList(WorldPackets::BattlePay::GetPurchaseList& getPurchaseList);
|
void HandleBattlePayGetPurchaseList(WorldPackets::BattlePay::GetPurchaseList& getPurchaseList);
|
||||||
void HandleBattlePayStartPurchase(WorldPackets::BattlePay::StartPurchase& startPurchase);
|
void HandleBattlePayStartPurchase(WorldPackets::BattlePay::StartPurchase& startPurchase);
|
||||||
void HandleBattlePayOpenCheckout(WorldPackets::BattlePay::OpenCheckout & openCheckout);
|
void HandleBattlePayOpenCheckout(WorldPackets::BattlePay::OpenCheckout & openCheckout);
|
||||||
|
void HandleCatalogShopLicenseGameDataRequest(WorldPackets::BattlePay::CatalogShopLicenseGameDataRequest& request);
|
||||||
void BattlePayProcessPurchase(uint32 productID);
|
void BattlePayProcessPurchase(uint32 productID);
|
||||||
void HandleBattlePayConfirmPurchaseResponse(WorldPackets::BattlePay::ConfirmPurchaseResponse& confirmPurchaseResponse);
|
void HandleBattlePayConfirmPurchaseResponse(WorldPackets::BattlePay::ConfirmPurchaseResponse& confirmPurchaseResponse);
|
||||||
void SendBattlePayDistributionList();
|
void SendBattlePayDistributionList();
|
||||||
|
|||||||
Reference in New Issue
Block a user