【问题标题】:Payments Lite (serverless): first purchase works, but the second always failsPayments Lite(无服务器):第一次购买有效,但第二次总是失败
【发布时间】:2018-01-06 02:27:22
【问题描述】:

在 Facebook 上作为 Canvas 应用程序托管的文字游戏中,我想出售消耗品“1 年 VIP 身份”,让玩家可以临时访问游戏中的某些区域 - 使用 Facebook Payments Lite (serverless)

我的 JavaScript 代码显示 支付对话框,然后将 signed_request 传递给我的 PHP 脚本 -

我的 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();
                });
        });
}

我的 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, '-_', '+/'));
}

在应用Dashboard -> Web Payments中,我添加了一个测试用户和一个“产品ID”test1的测试产品,价格为0.01欧元:

最后,我以测试用户身份登录并按下应用程序中调用buyVip 方法的按钮 - 导致 支付对话框 出现:

然后在服务器日志中我看到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
)

但是当我稍后尝试相同的过程时,支付对话框出现,但在按下 购买按钮后失败并出现错误

处理您的付款时出现问题:抱歉,我们遇到了 处理您的付款时遇到问题。您没有为此付费 交易。请重试。

在浏览器控制台中,我看到了1383001 Unknown 错误代码:

{error_code: 1383001, error_message: "处理有问题 您的付款:抱歉……已为此交易收取费用。请尝试 再次。”}

请问这是什么意思,为什么第一次购买请求成功,但随后失败?

在我的应用程序中,我当然会在成功购买后将“购买 VIP 状态”按钮隐藏一年,但我仍然想知道这里发生了什么。

另外,将来我想在我的游戏中出售诸如“硬币”之类的消耗性虚拟商品,然后多次购买应该会成功。

更新:

我已尝试通过将以下代码添加到我的payment.phpconsume 购买(使用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));

但不幸得到错误:

{"error":{"message":"不支持的发布请求。带有 ID 的对象 '498440660497153' 不存在,因缺失无法加载 权限,或不支持此操作。请阅读图表 API 文档位于 https://developers.facebook.com/docs/graph-api","type":"GraphMethodException","code":100,"fbtrace_id":"HDusTBubydJ"}}

【问题讨论】:

    标签: facebook dialog payment facebook-canvas


    【解决方案1】:

    在创建具有相同 product_id 的新用户之前,您应该使用该用户之前的购买。这样做是为了防止用户为非消耗品多次购买同一物品。

    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

    更新:

    如果您想通过服务器消费购买,您可以将 access_token 传递给您的 php 脚本。

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

    要获取 access_token,您可以使用它。

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

    【讨论】:

    • 谢谢你,阿列克谢!有没有办法在我的 payment.php 脚本中在服务器端消费购买?请参阅我的问题中的更新。我是否应该将用户访问令牌作为developer_payload 传递给解决方法?
    • 只需将 access_token 传递给您的脚本。查看我的答案的更新。
    【解决方案2】:

    我正在回答我自己的问题,以通过 Facebook Payments Lite 分享出售消耗品虚拟商品所需的完整源代码,基于 Alexey Mukhin 的有用回复 -

    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();
                                    });
                            }
                    });
            });
    }
    

    在您的网络服务器上托管的 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, '-_', '+/'));
    }
    

    注意:如果您碰巧有最新的 PHP 版本,那么最好在上面的代码中使用hash_equals,以减轻定时攻击

    不要忘记在您应用的Facebook Dashboard 中启用Payments Lite,并在其中添加“test1”产品:

    如果您按照上述说明操作,您将能够多次购买“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}
    

    最后,我将在我的 webhook 代码下方分享非精简版 Facebook Payments,因为这是我最终使用的(它处理退款并且不需要标记项目 消耗品购买后)-

    Facebook Canvas 应用中的 JavaScript 代码(分配给按钮-ONCLICK):

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

    在您的网络服务器上托管的 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
    }
    

    不要忘记在您的应用程序的Facebook Dashboard 中禁用 Payments Lite,并在其中添加“payment-full.php”网络挂钩:

    最后在您的网络服务器上添加“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>
    

    目前在网络上发现的 Facebook 支付示例并不多。

    如果您发现我的源代码(公共领域许可)有用,请点赞问题和答案,以帮助其他开发人员发现它。

    【讨论】:

      猜你喜欢
      • 2022-01-21
      • 1970-01-01
      • 2019-06-07
      • 2011-11-24
      • 2022-07-26
      • 1970-01-01
      • 1970-01-01
      • 2020-04-21
      • 1970-01-01
      相关资源
      最近更新 更多