简体   繁体   English

Payments Lite(无服务器):第一次购买有效,但第二次总是失败

[英]Payments Lite (serverless): first purchase works, but the second always fails

In a word game hosted as Canvas app at Facebook I would like to sell a consumable "1-year VIP status" giving players temporary access to certain areas in the game - by using the Facebook Payments Lite (serverless) . 在Facebook上作为Canvas应用托管的文字游戏中,我想出售一种消耗性的“ 1年VIP状态”,通过使用Facebook Payments Lite(无服务器)让玩家临时进入游戏中的某些区域。

My JavaScript code displays Pay Dialog and then passes signed_request to my PHP-script - 我的JavaScript代码显示“ 付款对话框” ,然后将signed_request传递给我的PHP脚本-

JavaScript code at my Canvas app: 我的Canvas应用中的JavaScript代码:

function buyVip() { 
        var obj = {
                method: "pay",
                action: "purchaseiap",
                product_id: "test1"
        };

        FB.ui(obj, function(data) {
                $.post("/payment-lite.php", 
                { signed_request: data.signed_request })
                .done(function(data) {
                        location.reload();
                });
        });
}

My PHP script /payment-lite.php: 我的PHP脚本/payment-lite.php:

const APP_SECRET = 'XXXXXXX';

$request = parse_signed_request($_POST['signed_request'], APP_SECRET);
error_log(print_r($request, TRUE));
// TODO validate $request and set the user VIP status in the game database

function parse_signed_request($signed_request, $secret) {
        list($encoded_sig, $payload) = explode('.', $signed_request, 2);
        $sig = base64_url_decode($encoded_sig);
        $data = json_decode(base64_url_decode($payload), TRUE);

        if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
                error_log('Unknown algorithm. Expected HMAC-SHA256');
                return NULL;
        }

        $expected_sig = hash_hmac('sha256', $payload, $secret, $raw = TRUE);
        if ($sig !== $expected_sig) {
                error_log('Bad Signed JSON signature!');
                return NULL;
        }
        return $data;
}

function base64_url_decode($input) {
        return base64_decode(strtr($input, '-_', '+/'));
}

In the app Dashboard -> Web Payments I have added a test user and a test product with "Product ID" test1 and the price of EUR 0.01: 在应用程序仪表板-> Web Payments中,我添加了一个测试用户和一个带有“产品ID” test1且价格为0.01欧元的测试产品:

仪表板

Finally I login as test user and press a button in the app calling the buyVip method - causing the Pay Dialog to appear: 最终,我以测试用户身份登录,然后在应用程序中按一个调用buyVip方法的按钮-导致出现“ 付款”对话框

支付对话框

Then in the server logs I see the payment.php script being called successfully: 然后在服务器日志中,我看到payment.php脚本被成功调用:

[30-Jul-2017 14:34:20 Europe/Berlin] Array
(
    [algorithm] => HMAC-SHA256
    [amount] => 0.01
    [app_id] => 376218039240910
    [currency] => EUR
    [issued_at] => 1501418059
    [payment_id] => 1084810821649513
    [product_id] => test1
    [purchase_time] => 1501418057
    [purchase_token] => 498440660497153
    [quantity] => 1
    [status] => completed
)

However when I try the same procedure later, the Pay Dialog appears, but then fails after pressing the Buy button with the error 但是,当我稍后尝试相同的过程时,出现“ 付款”对话框 ,但在按“ 购买”按钮后出现错误,然后失败

There Was a Problem Processing Your Payment: Sorry, but we're having trouble processing your payment. 处理您的付款时出现问题:抱歉,我们无法处理您的付款。 You have not been charged for this transaction. 您尚未为此交易付费。 Please try again. 请再试一次。

错误信息

And in the browser console I see the 1383001 Unknown error code: 在浏览器控制台中,我看到1383001未知错误代码:

{error_code: 1383001, error_message: "There Was a Problem Processing Your Payment: Sorry…n charged for this transaction. Please try again."} {error_code:1383001,error_message:“处理您的付款时出现问题:很抱歉,此交易已收取n费用。请重试。”}

What does it mean please, why do first buy requests succeed, but the subsequent fail? 请问是什么意思,为什么第一次购买请求成功,但是随后失败?

In my app I am of course going to hide the "buy VIP status" button for a year after successful purchase, but still I would like to know, what is happening here. 在我的应用程序中,我当然会在成功购买后隐藏“购买VIP身份”按钮一年,但是我仍然想知道这里发生了什么。

Also in the future I would like to sell consumable virtual goods like "coins" in my game and then multiple purchases should succeed. 同样,将来我想在游戏中出售诸如“硬币”之类的消耗性虚拟商品,然后多次购买应会成功。

UPDATE: 更新:

I have tried to consume the purchase by adding the following code to my payment.php (using APP_ID|APP_SECRET instead of the required user access token): 我尝试通过将以下代码添加到我的payment.php (使用APP_ID | APP_SECRET代替所需的用户访问令牌)来消耗购买的商品:

$post = [
    'access_token' => APP_ID . '|' . APP_SECRET,
];

$ch = curl_init('https://graph.facebook.com/498440660497153/consume');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$response = curl_exec($ch);
curl_close($ch);
error_log(print_r($response, TRUE));

But unfortunately get the error: 但不幸的是得到了错误:

{"error":{"message":"Unsupported post request. Object with ID '498440660497153' does not exist, cannot be loaded due to missing permissions, or does not support this operation. Please read the Graph API documentation at https://developers.facebook.com/docs/graph-api","type":"GraphMethodException","code":100,"fbtrace_id":"HDusTBubydJ"}} {“ error”:{“ message”:“不支持的发布请求。ID为'498440660497153'的对象不存在,由于权限丢失而无法加载,或不支持此操作。请阅读Graph API文档,网址为https:// /developers.facebook.com/docs/graph-api“,”类型“:” GraphMethodException“,”代码“:100,” fbtrace_id“:” HDusTBubydJ“}}

You should consume previous purchase for that user before creating new one with same product_id. 在创建具有相同product_id的新商品之前,您应该为该用户使用以前的购买商品。 This is done to prevent users from buying the same item more than once for non consumable item. 这样做是为了防止用户为非消耗性物品多次购买同一物品。

FB.api(
  '/' + PURCHASE_TOKEN + '/consume',    // Replace the PURCHASE_TOKEN
  'post',
  {access_token: access_token},         // Replace with a user access token
  result => {
    console.log('consuming product', productId, 'with purchase token', purchaseToken);
    console.log('Result:');
    console.log(result);
  }
);

https://developers.facebook.com/docs/games_payments/payments_lite#consuming https://developers.facebook.com/docs/games_payments/payments_lite#using

UPDATE: 更新:

If you want to consume purchase via server you can pass access_token to your php script. 如果您想通过服务器消费,可以将access_token传递给您的php脚本。

$.post("/words/facebook/payment.php", { access_token: access_token })        

To get access_token you can use this. 要获取access_token,您可以使用它。

var access_token = '';
FB.getLoginStatus(function(response) {
  if (response.status === 'connected') {
    access_token = response.authResponse.accessToken;
  }
});

I am answering my own question to share the complete source code needed to sell consumable virtual goods via Facebook Payments Lite , based on Alexey Mukhin's helpful reply - 我正在回答自己的问题,以根据Alexey Mukhin的有用答复分享通过Facebook Payments Lite出售消费性虚拟商品所需的完整源代码-

JavaScript code in your Facebook Canvas app (assign to a button-ONCLICK): 您的Facebook Canvas应用中的JavaScript代码(分配给按钮-ONCLICK):

function buyItemLite() { 
        var payDialog = {
                method: "pay",
                action: "purchaseiap",
                product_id: "test1"
        };

        FB.ui(payDialog, function(payResponse) {
                FB.getLoginStatus(function(loginResponse) {
                        if (loginResponse.status === "connected") {
                                $.post("/payment-lite.php", {
                                        signed_request: payResponse.signed_request,
                                        access_token: loginResponse.authResponse.accessToken 
                                })
                                .done(function(consumeResponse) {
                                        location.reload();
                                });
                        }
                });
        });
}

PHP code in the payment-lite.php script hosted at your web server: Web服务器上托管的payment-lite.php脚本中的PHP代码:

const APP_ID              = 'replace by your app id';
const APP_SECRET          = 'replace by your app secret';
const SIGNED_REQUEST      = 'signed_request';
const STATUS              = 'status';
const COMPLETED           = 'completed';
const PRODUCT_ID          = 'product_id';
const PURCHASE_TOKEN      = 'purchase_token';
const ACCESS_TOKEN        = 'access_token';
const CONSUME_URL         = 'https://graph.facebook.com/%d/consume';

$request = parse_signed_request($_REQUEST[SIGNED_REQUEST], APP_SECRET);
error_log('pay dialog request: ' . print_r($request, TRUE));

if ($request[STATUS] === COMPLETED && $request[PRODUCT_ID] === 'test1') {
        # perform POST request to consume the purchase_token
        $url = sprintf(CONSUME_URL, $request[PURCHASE_TOKEN]);
        $fields = array(ACCESS_TOKEN => $_REQUEST[ACCESS_TOKEN]);
        $client = curl_init($url);
        curl_setopt($client, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($client, CURLOPT_POSTFIELDS, $fields);
        $response = curl_exec($client);
        curl_close($client);
        error_log('consume response: ' . print_r($response, TRUE));
        # TODO give the player the newly purchased consumable "test1" product
}

function parse_signed_request($signed_request, $secret) {
        list($encoded_sig, $payload) = explode('.', $signed_request, 2);
        $sig = base64_url_decode($encoded_sig);
        $data = json_decode(base64_url_decode($payload), TRUE);
        if (strtoupper($data['algorithm']) !== 'HMAC-SHA256') {
                error_log('Unknown algorithm. Expected HMAC-SHA256');
                return NULL;
        }

        $expected_sig = hash_hmac('sha256', $payload, $secret, $raw = TRUE);
        if ($sig !== $expected_sig) { // or better use hash_equals
                error_log('Bad Signed JSON signature!');
                return NULL;
        }
        return $data;
}

function base64_url_decode($input) {
        return base64_decode(strtr($input, '-_', '+/'));
}

NOTE: If you happen to have a recent PHP version, then better use hash_equals in the above code, to mitigate timing attacks . 注意:如果您碰巧具有最新的PHP版本,则最好在上面的代码中使用hash_equals ,以减轻计时攻击

Do not forget to enable Payments Lite in the Facebook Dashboard of your app and also add a "test1" product there: 不要忘记在应用程序的Facebook仪表板中启用Payments Lite,并在其中添加“ test1”产品:

仪表板

If you follow the above instructions you will be able to purchase the "test1" item multiple times and the output you will get in the PHP logs will look like: 如果您按照上述说明进行操作,则可以多次购买“ test1”项目,并且您在PHP日志中获得的输出将类似于:

pay dialog request: Array
(
    [algorithm] => HMAC-SHA256
    [amount] => 0.01
    [app_id] => 376218039240910
    [currency] => EUR
    [issued_at] => 1501674845
    [payment_id] => 1041009052696057
    [product_id] => test1
    [purchase_time] => 1501674843
    [purchase_token] => 499658830375336
    [quantity] => 1
    [status] => completed
)

consume response: {"success":true}

Finally, I will share below my webhook code for non-lite Facebook Payments , because that is what I actually have ended up using (it handles chargebacks and does not need to mark items consumable after purchases) - 最后,我将在我的webhook代码下面分享非轻型Facebook Payments ,因为这实际上是我最终使用的代码(它可以处理退款,并且不需要在购买后标记消耗品 )-

JavaScript code in your Facebook Canvas app (assign to a button-ONCLICK): 您的Facebook Canvas应用中的JavaScript代码(分配给按钮-ONCLICK):

function buyItemFull() { 
        var payDialog = {
                method:  "pay",
                action:  "purchaseitem",
                product: "https://myserver/test1.html"
        };

        FB.ui(payDialog, function(data) {
                location.reload();
        });
}

PHP code in the payment-full.php script hosted at your web server: Web服务器上托管的payment-full.php脚本中的PHP代码:

const APP_ID              = 'replace by your app id';
const APP_SECRET          = 'replace by your app secret';

const HUB_MODE            = 'hub_mode';
const HUB_CHALLENGE       = 'hub_challenge';
const HUB_VERIFY_TOKEN    = 'hub_verify_token';
const SUBSCRIBE           = 'subscribe';

const ENTRY               = 'entry';
const CHANGED_FIELDS      = 'changed_fields';
const ID                  = 'id';
const USER                = 'user';
const ACTIONS             = 'actions';
const ITEMS               = 'items';
const PRODUCT             = 'product';
const AMOUNT              = 'amount';

# payment status can be initiated, failed, completed
const STATUS              = 'status';
const COMPLETED           = 'completed';

# possible payment event types are listed below
const TYPE                = 'type';
const CHARGE              = 'charge';
const CHARGEBACK_REVERSAL = 'chargeback_reversal';
const REFUND              = 'refund';
const CHARGEBACK          = 'chargeback';
const DECLINE             = 'decline';

const GRAPH               = 'https://graph.facebook.com/v2.10/%d?access_token=%s|%s&fields=user,actions,items';
const TEST1               = 'https://myserver/test1.html';

# called by Facebook Dashboard when "Test Callback URL" button is pressed
if (isset($_GET[HUB_MODE]) && $_GET[HUB_MODE] === SUBSCRIBE) {
        print($_GET[HUB_CHALLENGE]);
        exit(0);
}

# called when there is an update on a payment (NOTE: better use hash_equals)
$body = file_get_contents('php://input');
if ('sha1=' . hash_hmac('sha1', $body, APP_SECRET) != $_SERVER['HTTP_X_HUB_SIGNATURE']) {
        error_log('payment sig=' . $_SERVER['HTTP_X_HUB_SIGNATURE'] . ' does not match body=' . $body);
        exit(1);
}

# find the updated payment id and what has changed: actions or disputes
$update         = json_decode($body, TRUE);
error_log('payment update=' . print_r($update, TRUE));
$entry          = array_shift($update[ENTRY]);
$payment_id     = $entry[ID];
$changed_fields = $entry[CHANGED_FIELDS];

if (!in_array(ACTIONS, $changed_fields)) {
        error_log('payment actions has not changed');
        exit(0);
}

# fetch the updated payment details: user, actions, items
$graph   = sprintf(GRAPH, $payment_id, APP_ID, APP_SECRET);
$payment = json_decode(file_get_contents($graph), TRUE);
error_log('payment details=' . print_r($payment, TRUE));

# find the user id who has paid
$uid     = $payment[USER][ID];

# find the last action and its status and type
$actions = $payment[ACTIONS];
$action  = array_pop($actions);
$status  = $action[STATUS];
$type    = $action[TYPE];
$price   = $action[AMOUNT];

# find which product was purchased
$items   = $payment[ITEMS];
$item    = array_pop($items);
$product = $item[PRODUCT];
error_log("payment uid=$uid status=$status type=$type product=$product price=$price");

if ($status != COMPLETED) {
        error_log('payment status is not completed');
        exit(0);
}

# money has been received, update the player record in the database
if ($type === CHARGE || $type === CHARGEBACK_REVERSAL) {
        if ($product === TEST1) {
                # TODO give the player the purchased "test1" product
        }
} else if ($type === REFUND || $type === CHARGEBACK || $type === DECLINE) {
        # TODO take away from the player the "test1" product
}

Do not forget to disable Payments Lite in the Facebook Dashboard of your app and also add the "payment-full.php" webhook there: 不要忘记在应用程序的Facebook仪表板中禁用Payments Lite ,并在此处添加“ payment-full.php” webhook:

仪表板

Finally add the "test1.html" product file at your web server: 最后,在您的Web服务器上添加“ test1.html”产品文件:

<!DOCTYPE html><html>
 <head prefix=
    "og: http://ogp.me/ns# 
     fb: http://ogp.me/ns/fb# 
     product: http://ogp.me/ns/product#">
    <meta property="og:type"                content="og:product" />
    <meta property="og:title"               content="Test1" />
    <meta property="og:image"               content="https://myserver/icon-50x50.png" />
    <meta property="og:description"         content="Test1" />
    <meta property="og:url"                 content="https://myserver/test1.html" />
    <meta property="product:price:amount"   content="0.01"/>
    <meta property="product:price:currency" content="EUR"/>
  </head>
</html>

There are currently not many Facebook Payment examples to be discovered on the web. 当前在网络上没有很多Facebook付款示例。

So upvote the question and the answer, if you have found my source code (public domain license) useful, to help other developers to discover it. 因此,如果您发现我的源代码(公共领域许可)很有用,请对问题和答案进行投票,以帮助其他开发人员发现它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM