Skip to content

Commit

Permalink
NEXT-36303 - In App Purchases docs (#1641)
Browse files Browse the repository at this point in the history
* NEXT-36303 - In App Purchases docs

* Small grammar fixes

---------

Co-authored-by: Micha <[email protected]>
  • Loading branch information
lernhart and Isengo1989 authored Feb 6, 2025
1 parent 050b134 commit 38783d3
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 3 deletions.
6 changes: 3 additions & 3 deletions guides/plugins/apps/gateways/checkout/checkout-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ The app server will receive the current `SalesChannelContext`, `Cart`, and avail
**Connection timeouts**

The Shopware shop will wait for a response for 5 seconds.
Be sure, that your checkout gateway implementation on your app server responds in time, otherwise Shopware will time out and drop the connection.
Be sure that your checkout gateway implementation on your app server responds in time, otherwise Shopware will time out and drop the connection.
:::

Your app server can then respond with a list of commands to manipulate the cart, payment methods, shipping methods, or add cart errors.

You can find a reference of all currently available commands [here](./command-reference.md).

Let's assume for this example, that your payment method is not available for carts with a total price above 1000€.
Let's assume that your payment method is not available for carts with a total price above 1000€.

<Tabs>

Expand Down Expand Up @@ -183,7 +183,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('gateway', name: 'braintree.gateway.')]
#[Route('/api/gateway', name: 'api.gateway.')]
class GatewayController extends AbstractController
{
public function __construct(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# In-App Purchase Gateway

## Context

Starting with Shopware version **6.6.9.0**, the In-App Purchase Gateway was introduced to enhance flexibility in managing In-App Purchases.

The gateway enables app servers to restrict specific In-App Purchases based on advanced decision-making processes handled on the app server side.

::: info
**Current Limitations:**
At present, the In-App Purchase Gateway supports only restricting the checkout process for new In-App Purchases.
**Plans:**
We aim to expand its functionality to include filtering entire lists of In-App Purchases before they are displayed to users.
:::

## Prerequisites

You should be familiar with the concept of Apps, their registration flow as well as signing and verifying requests and responses between Shopware and the App backend server.

<PageRef page="../../app-base-guide.md" title="App base guide" />

Your app server must be also accessible for the Shopware server.
You can use a tunneling service like [ngrok](https://ngrok.com/) for development.

## Manifest Configuration

To indicate that your app leverages the In-App Purchase Gateway, include the `inAppPurchase` property within the `gateways` property in your app's `manifest.xml`.

Below is an example of a properly configured manifest snippet for enabling the checkout gateway:

```xml [manifest.xml]
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<!-- ... -->

<gateways>
<inAppPurchases>https://my-app.server.com/inAppPurchases/gateway</inAppPurchases>
</gateways>
</manifest>
```

After successful installation of your app, the In-App Purchases gateway will already be used.

## In-App Purchases gateway endpoint

During checkout of an In-App Purchase, Shopware checks for any active In-App Purchases gateways and will call the `inAppPurchases` url.
The app server will receive a list containing the single only In-App Purchase the user wants to buy as part of the payload.

::: warning
**Connection timeouts**

The Shopware shop will wait for a response for 5 seconds.
Be sure that your In-App Purchases gateway implementation on your app server responds in time,
otherwise Shopware will time out and drop the connection.
:::

<Tabs>

<Tab title="HTTP">

Request content is JSON

```json5
{
"source": {
"url": "http:\/\/localhost:8000",
"shopId": "hRCw2xo1EDZnLco4",
"appVersion": "1.0.0",
"inAppPurchases": "eyJWTEncodedTokenOfActiveInAppPurchases"
},
"purchases": [
"my-in-app-purchase-bronze",
"my-in-app-purchase-silver",
"my-in-app-purchase-gold",
],
}
```

Respond with the In-App Purchases you want the user to be allowed to buy by simply responding with the purchase identifier in the `purchases` array.
During checkout, respond with an empty array to disallow the user from buying the In-App Purchase.

```json5
{
"purchases": [
"my-in-app-purchase-bronze",
"my-in-app-purchase-silver",
// disallow the user from buying the gold in-app purchase by removing it from the response
]
}
```

</Tab>

<Tab title="App PHP SDK">

With version `4.0.0`, support for the In-App Purchases gateway has been added to the `app-php-sdk`.
The SDK will handle the communication with the Shopware shop and provide you with a convenient way to handle the incoming payload and respond with the necessary purchases.

```php

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Shopware\App\SDK\Authentication\ResponseSigner;
use Shopware\App\SDK\Context\Cart\Error;
use Shopware\App\SDK\Context\ContextResolver;
use Shopware\App\SDK\Context\InAppPurchase\InAppPurchaseProvider;
use Shopware\App\SDK\Framework\Collection;
use Shopware\App\SDK\HttpClient\ClientFactory;
use Shopware\App\SDK\Response\InAppPurchaseResponse;
use Shopware\App\SDK\Shop\ShopResolver;

function inAppPurchasesController(): ResponseInterface {
// injected or build by yourself
$shopResolver = new ShopResolver($repository);
$signer = new ResponseSigner();

$shop = $shopResolver->resolveShop($request);

$inAppPurchaseProvider = new InAppPurchaseProvider(new SBPStoreKeyFetcher(
(new ClientFactory())->createClient($shop)
));

$contextResolver = new ContextResolver($inAppPurchaseProvider);

/** @var Shopware\App\SDK\Context\Gateway\InAppFeatures\FilterAction $action */
$action = $contextResolver->assembleInAppPurchasesFilterRequest($request, $shop);

/** @var Shopware\App\SDK\Framework\Collection $purchases */
$purchases = $action->getPurchases();

// filter the purchases based on your business logic
$purchases->remove('my-in-app-purchase-gold');

$response = InAppPurchasesResponse::filter($purchases);

return $signer->sign($response);
}
```

</Tab>

<Tab title="Symfony Bundle">

```php
<?php declare(strict_types=1);

namespace App\Controller;

use Shopware\App\SDK\Context\Cart\Error;
use Shopware\App\SDK\Context\Gateway\InAppFeatures\FilterAction;
use Shopware\App\SDK\Framework\Collection;
use Shopware\App\SDK\Gateway\Checkout\CheckoutGatewayCommand;
use Shopware\App\SDK\Gateway\Checkout\Command\AddCartErrorCommand;
use Shopware\App\SDK\Response\GatewayResponse;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/api/gateway', name: 'api.gateway.')]
class GatewayController extends AbstractController
{
public function __construct(
private readonly HttpFoundationFactoryInterface $httpFoundationFactory
) {
}

#[Route('/inAppPurchases', name: 'in-app-purchases', methods: ['POST'])]
public function inAppPurchases(FilterAction $action): Response
{
// the user already has the best premium purchase
// disallow him from buying the less premium ones
if ($action->source->inAppPurchases->has('my-in-app-purchase-gold')) {
$action->purchases->remove('my-in-app-purchase-bronze');
$action->purchases->remove('my-in-app-purchase-silver');
}

$response = GatewayResponse::createCheckoutGatewayResponse($commands);

return $this->httpFoundationFactory->createResponse($response);
}
}
```

</Tab>

</Tabs>

## Event

Plugins can listen to the `Shopware\Core\Framework\App\InAppPurchases\Event\InAppPurchasesGatewayEvent`.
This event is dispatched after the In-App Purchases Gateway has received the app server response.
It allows plugins to manipulate the available In-App Purchases, based on the same payload the app servers retrieved.
82 changes: 82 additions & 0 deletions guides/plugins/apps/in-app-purchase/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# In-App Purchases

In-App Purchases are a way to lock certain features behind a paywall within the same extension.
This is useful for developers who want to offer a free version of their extension with limited features and a paid version with more features.

## Retrieve In-App Purchases on your app server

Whenever Shopware sends you a request, you'll receive a JWT as a query parameter or in the request body,
depending on whether the request is a GET or POST.
This JWT is signed by our internal systems, ensuring that you, as the app developer, can verify its authenticity and confirm it hasn't been tampered with.

You can use the `shopware/app-php-sdk` for plain PHP or the `shopware/app-bundle` for Symfony to validate and decode the JWT.
An example for plain PHP is available [here](https://github.com/shopware/app-php-sdk/blob/main/examples/index.php).
For Symfony applications, use the appropriate action argument for your route.

### Admin

You will also receive In-App Purchases with the initial `sw-main-hidden` admin request.
To make them accessible, inject them into your JavaScript application.

Here is an example of retrieving active In-App Purchases in an example `admin.html.twig` using the `shopware/app-bundle`:

```php
#[Route(path: '/app/admin', name: 'admin')]
public function admin(ModuleAction $action): Response {
return $this->render('admin.html.twig', [
'inAppPurchases' => $action->inAppPurchases->all(),
]);
}
```

```html
<!DOCTYPE html>
<html>
<head>
<script>
try {
window.inAppPurchases = JSON.parse('{{ inAppPurchases | json_encode | raw }}');
} catch (e) {
window.inAppPurchases = [];
console.error('Unable to decode In-App Purchases', e);
}
</script>

<!-- ... -->
</head>

<!-- ... -->
</html>
```

Alternatively you can extract the query parameter from `document.location` on the initial `sw-main-hidden` request,
store it and ask your app-server do properly decode it for you.

## Trigger a purchase of an In-App Purchases

The checkout process itself is provided by Shopware, you only have to trigger it with an identifier of the In-App Purchase.
To do so, create a button and make use of the [Meteor Admin SDK](https://github.com/shopware/meteor/tree/main/packages/admin-sdk):

```vue
<template>
<!-- ... -->
<p>
If you buy this you'll get an incredible useful feature: ...
</p>
<mt-button @click="onClick">
Buy
</mt-button>
<!-- ... -->
</template>
<script setup>
import * as sw from '@shopware/meteor-admin-sdk';
function onClick() {
sw.iap.purchase({ identifier: 'my-iap-identifier' });
}
</script>
```

Alternatively, you can trigger a checkout manually by sending a properly formatted
[post message](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) with an In-App purchase identifier to the Admin.
59 changes: 59 additions & 0 deletions guides/plugins/plugins/in-app-purchase/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# In-App Purchases

In-App Purchases are a way to lock certain features behind a paywall within the same extension.
This is useful for developers who want to offer a free version of their extension with limited features,
and then offer a paid version with more features.

## Active In-App Purchases

The `InAppPurchase` class contains a list of all In-App Purchases.
Inject this service into your class and you can check against it:

```php
class Example
{
public function __construct(
private readonly InAppPurchase $inAppPurchase,
) {}

public function someFunction() {
if ($this->inAppPurchase->isActive('MyExtensionName', 'my-iap-identifier')) {
// ...
}

// ...
}
}
```

If you want to check an in-app purchase in the administration:

```js
if (Shopware.InAppPurchase.isActive('MyExtensionName', 'my-iap-identifier')) {};
```

## Allow users to buy an In-App Purchase

```js
{
computed: {
inAppPurchaseCheckout() {
return Shopware.Store.get('inAppPurchaseCheckout');
}
},

methods: {
onClick() {
this.inAppPurchaseCheckout.request({ identifier: 'my-iap-identifier' }, 'MyExtensionName');
}
}
}
```

## Event

Apps are also able to manipulate the available In-App Purchases as described in <PageRef page="../../apps/gateways/in-app-purchase/in-app-purchase-gateway.md" title="In App purchase gateway" />.

Plugins can listen to the `Shopware\Core\Framework\App\InAppPurchases\Event\InAppPurchasesGatewayEvent`.
This event is dispatched after the In-App Purchases Gateway has received the app server response from a gateway
and allows plugins to manipulate the available In-App Purchases.

0 comments on commit 38783d3

Please sign in to comment.