facebookdialogpaymentfacebook-canvas

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:

dashboard

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

pay dialog

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.

error message

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


Solution

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