简体   繁体   中英

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) .

My JavaScript code displays Pay Dialog and then passes signed_request to my PHP-script -

JavaScript code at my Canvas app:

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:

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:

仪表板

Finally I login as test user and press a button in the app calling the buyVip method - causing the Pay Dialog to appear:

支付对话框

Then in the server logs I see the payment.php script being called successfully:

[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:

{error_code: 1383001, error_message: "There Was a Problem Processing Your Payment: Sorry…n charged for this transaction. Please try again."}

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.

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):

$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"}}

You should consume previous purchase for that user before creating new one with same 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

UPDATE:

If you want to consume purchase via server you can pass access_token to your php script.

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

To get access_token you can use this.

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 -

JavaScript code in your Facebook Canvas app (assign to a button-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:

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 .

Do not forget to enable Payments Lite in the Facebook Dashboard of your app and also add a "test1" product there:

仪表板

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:

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) -

JavaScript code in your Facebook Canvas app (assign to a button-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:

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:

仪表板

Finally add the "test1.html" product file at your web server:

<!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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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