Платежи Lite (бессерверные): первая покупка работает, а вторая всегда терпит неудачу

В словесной игре, размещенной как приложение Canvas на Facebook, я хотел бы продавать расходуемый «VIP-статус на 1 год», дающий игрокам временный доступ к определенным областям в игре — с помощью Платежи Facebook Lite (без сервера).

Мой код JavaScript отображает Диалог оплаты, а затем передает signed_request в мой PHP-скрипт -

Код JavaScript в моем приложении Canvas:

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

В приложении Панель управления -> Веб-платежи я добавил тестового пользователя и тестовый продукт с "Идентификатором продукта" 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: "При обработке вашего платежа возникла проблема: извините… за эту транзакцию взимается плата. Повторите попытку."}

Что это значит, пожалуйста, почему первые запросы на покупку выполняются, а последующие терпят неудачу?

В своем приложении я конечно же собираюсь скрыть кнопку "купить вип статус" на год после удачной покупки, но все же хотелось бы знать, что тут происходит.

Также в будущем я хотел бы продавать расходные виртуальные товары, такие как «монеты», в своей игре, и тогда несколько покупок должны быть успешными.

ОБНОВЛЕНИЕ:

Я попытался использовать покупку, добавив следующий код в свой 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));

Но, к сожалению, получить ошибку:

{"error":{"message":"Неподдерживаемый почтовый запрос. Объект с идентификатором '498440660497153' не существует, не может быть загружен из-за отсутствия разрешений или не поддерживает эту операцию. Пожалуйста, прочтите документацию Graph API по адресу https:/ /developers.facebook.com/docs/graph-api","type":"GraphMethodException","code":100,"fbtrace_id":"HDusTBubydJ"}}


person Alexander Farber    schedule 30.07.2017    source источник


Ответы (2)


Вы должны использовать предыдущую покупку для этого пользователя, прежде чем создавать новую с тем же 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#using

ОБНОВЛЕНИЕ:

Если вы хотите использовать покупку через сервер, вы можете передать 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;
  }
});
person Alexey Mukhin    schedule 30.07.2017
comment
Спасибо, Алексей! Есть ли способ использовать покупку на стороне сервера в моем сценарии payment.php? Пожалуйста, смотрите ОБНОВЛЕНИЕ в моем вопросе. Должен ли я передать токен доступа пользователя как developer_payload в качестве обходного пути? - person Alexander Farber; 31.07.2017
comment
Просто передайте access_token в свой скрипт. Смотрите обновление моего ответа. - person Alexey Mukhin; 02.08.2017

Я отвечаю на свой вопрос, чтобы поделиться полным исходным кодом, необходимым для продажи расходуемых виртуальных товаров через Facebook Payments Lite, основанный на полезном ответе Алексея Мухина -

Код JavaScript в вашем приложении Facebook Canvas (назначьте кнопку — НАЖМИТЕ ПО НАЖАТИЮ):

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 в скрипте payment-lite.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 в приведенном выше коде, чтобы смягчить временные атаки.

Не забудьте включить Payments Lite на панели управления Facebook вашего приложения. а также добавить туда товар "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, потому что это то, что я на самом деле использовал (он обрабатывает возвратные платежи и не нужно помечать предметы расходуемые после покупки) -

Код JavaScript в вашем приложении Facebook Canvas (назначьте кнопку — НАЖМИТЕ ПО НАЖАТИЮ):

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

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

Код PHP в сценарии payment-full.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
}

Не забудьте отключить Payments Lite на панели управления Facebook вашего приложения. а также добавить туда вебхук «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.

Так что проголосуйте за вопрос и ответ, если вы нашли мой исходный код (лицензия общественного достояния) полезным, чтобы помочь другим разработчикам открыть его.

person Alexander Farber    schedule 02.08.2017