-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEXT-36303 - In App Purchases docs (#1641)
* NEXT-36303 - In App Purchases docs * Small grammar fixes --------- Co-authored-by: Micha <[email protected]>
- Loading branch information
1 parent
050b134
commit 38783d3
Showing
4 changed files
with
337 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 193 additions & 0 deletions
193
guides/plugins/apps/gateways/in-app-purchase/in-app-purchase-gateway.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |