Skip to content

Commit

Permalink
NEXT-36303 - In app purchases (#27)
Browse files Browse the repository at this point in the history
* EXS-126 - Added In-App purchase modal

* EXS-126 - Fixed PHPStan errors and linter issues

* EXS-126 - Added tests for checkout-button, checkout-state, overview and price-box;

* NEXT - 37408 - IAP badge

* EXS-127 - Added listing modal for IAPs

* EXS-127 - Listing modal completed

* EXS-127 - Logic to get the list

* EXS-127 - Design and phpstan changes

* EXS-127 - Updated code to use id not string

* EXS-127 - Added check for IAP's before calling list

* EXS-127 - Styling updates

* EXS-127 - Update URL's

* EXS-128 - Update endpoints and data types

* NEXT-36303 - Small changes to get code in line with SBP and fix some issues

* EXS-127 - Moved translations

* NEXT-38473 - Refresh IAP's after purchase

* NEXT-36303 - Fixed an issue where the return type wasn't valid

* NEXT-36303 - Fixed admin install

* NEXT-37409 - Add licensed IAP flag and badge

* NEXT-36303 - Code review improvements

* NEXT-38822 - Add filter gateway to list and checkout calls

* NEXT-37049 - Add licensed iap flag

* NEXT-39485 - Fixed gateway

* NEXT-39467 - Replaced syncer with updater

* NEXT-39461 - Changed handling of updated response

* NEXT-36303 - Github workflow changes

* NEXT-36303 - Git workflow updates by pweyck

* NEXT-36303 - Code review changes

* NEXT-36303 - Linter changes

* NEXT-36303 - Test changes and fixes

---------

Co-authored-by: Michel Bade <[email protected]>
Co-authored-by: Albert Scherman <[email protected]>
Co-authored-by: Patrick Weyck <[email protected]>
Co-authored-by: Albert Scherman <[email protected]>
Co-authored-by: Philip Reinken <[email protected]>
Co-authored-by: Max Stegmeyer <[email protected]>
  • Loading branch information
7 people authored Dec 2, 2024
1 parent 1294c2c commit 362dd7b
Show file tree
Hide file tree
Showing 64 changed files with 4,293 additions and 846 deletions.
34 changes: 28 additions & 6 deletions .github/workflows/admin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,40 @@ on:
branches:
- trunk
pull_request:
workflow_dispatch:

env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 'true'
COMPOSER_ROOT_VERSION: 6.6.9999999-dev

jobs:
unit:
name: Jest
uses: shopware/github-actions/.github/workflows/admin-jest.yml@main
with:
extensionName: ${{ github.event.repository.name }}
uploadCoverage: true
secrets:
codecovToken: ${{ secrets.CODECOV_TOKEN }}
runs-on: ubuntu-latest
steps:
- name: Setup extension
uses: shopware/github-actions/setup-extension@main
with:
extensionName: SwagExtensionStore
shopwareVersion: trunk
install-admin: true

- name: Jest Unit Tests
shell: bash
working-directory: custom/plugins/SwagExtensionStore/src/Resources/app/administration
run: npm run unit -- --coverage

- name: Upload Coverage
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
root_dir: ${{ github.workspace }}/custom/plugins/SwagExtensionStore
working-directory: ${{ github.workspace }}/custom/plugins/SwagExtensionStore
directory: ${{ github.workspace }}/custom/plugins/SwagExtensionStore/src/Resources/app/administration
lint:
name: ESLint
uses: shopware/github-actions/.github/workflows/admin-eslint.yml@main
with:
extensionName: ${{ github.event.repository.name }}
shopwareVersion: trunk
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ includes:
parameters:
phpVersion: 80200
level: 8
tmpDir: var/cache/phpstan
tmpDir: ../../../var/cache/phpstan
treatPhpDocTypesAsCertain: false
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
Expand Down
140 changes: 140 additions & 0 deletions src/Controller/InAppPurchasesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

declare(strict_types=1);

namespace SwagExtensionStore\Controller;

use Shopware\Core\Framework\App\AppCollection;
use Shopware\Core\Framework\App\AppEntity;
use Shopware\Core\Framework\App\InAppPurchases\Gateway\InAppPurchasesGateway;
use Shopware\Core\Framework\App\InAppPurchases\Payload\InAppPurchasesPayload;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Store\InAppPurchase\Services\InAppPurchaseUpdater;
use Shopware\Core\Framework\Store\Services\AbstractExtensionDataProvider;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use SwagExtensionStore\Exception\ExtensionStoreException;
use SwagExtensionStore\Services\InAppPurchasesService;
use SwagExtensionStore\Struct\InAppPurchaseCartPositionCollection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

/**
* @internal
*/
#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['api']])]
class InAppPurchasesController
{
/**
* @param EntityRepository<AppCollection> $appRepository
*/
public function __construct(
private readonly InAppPurchasesService $inAppPurchasesService,
private readonly InAppPurchaseUpdater $inAppPurchaseUpdater,
private readonly AbstractExtensionDataProvider $extensionDataProvider,
private readonly InAppPurchasesGateway $appPurchasesGateway,
private readonly EntityRepository $appRepository,
) {}

#[Route('/api/_action/in-app-purchases/{technicalName}/details', name: 'api.in-app-purchases.detail', methods: ['GET'])]
public function getInAppFeature(string $technicalName, Context $context): Response
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', $technicalName));

$extension = $this->extensionDataProvider
->getInstalledExtensions($context, false, $criteria)
->first();

return new JsonResponse($extension);
}

#[Route('/api/_action/in-app-purchases/cart/new', name: 'api.in-app-purchases.cart.new', methods: ['POST'])]
public function createCart(RequestDataBag $data, Context $context): Response
{
$name = $data->getString('name');
$feature = $data->getString('feature');

$cart = $this->inAppPurchasesService->createCart($name, $feature, $context);

return new JsonResponse($cart);
}

#[Route('/api/_action/in-app-purchases/cart/order', name: 'api.in-app-purchases.cart.order', methods: ['POST'])]
public function orderCart(RequestDataBag $data, Context $context): Response
{
$taxRate = \floatval($data->getString('taxRate'));
$positions = $data->get('positions');
$extensionName = $data->get('name');

$positionCollection = InAppPurchaseCartPositionCollection::fromArray($positions->all());

$app = $this->getAppByName($extensionName, $context);
if (!$app) {
// if no app is found, it's a plugin, and no filtering will happen
return $this->inAppPurchasesService->orderCart($taxRate, $positionCollection->toCart(), $context);
}

$payload = new InAppPurchasesPayload($positionCollection->getIdentifiers());
$iapGatewayResponse = $this->appPurchasesGateway->process($payload, $context, $app);
if (!$iapGatewayResponse) {
// if $iapGatewayResponse is null, the app does not have a gateway url, and no filtering will happen
return $this->inAppPurchasesService->orderCart($taxRate, $positionCollection->toCart(), $context);
}

$positionCollection = $positionCollection->filterValidInAppPurchases($positionCollection, $iapGatewayResponse->purchases);
if ($positionCollection->count() === 0) {
throw ExtensionStoreException::invalidInAppPurchase();
}

return $this->inAppPurchasesService->orderCart($taxRate, $positionCollection->toCart(), $context);
}

#[Route('/api/_action/in-app-purchases/{extensionName}/list', name: 'api.in-app-purchase.list', methods: ['GET'])]
public function listPurchases(string $extensionName, Context $context): Response
{
$purchases = $this->inAppPurchasesService->listPurchases($extensionName, $context);

$app = $this->getAppByName($extensionName, $context);
if (!$app) {
return new JsonResponse($purchases);
}

$payload = new InAppPurchasesPayload($purchases->getIdentifiers());
$validCartItems = $this->appPurchasesGateway->process($payload, $context, $app);
if (!$validCartItems) {
return new JsonResponse($purchases);
}

$purchases = $purchases->filterValidInAppPurchases($purchases, $validCartItems->purchases);
if ($purchases->count() === 0) {
throw ExtensionStoreException::invalidInAppPurchase();
}

return new JsonResponse($purchases);
}

#[Route('/api/_action/in-app-purchases/refresh', name: 'api.in-app-purchase.refresh', methods: ['GET'])]
public function refreshInAppPurchases(Context $context): Response
{
$context->scope(Context::SYSTEM_SCOPE, function (Context $context) {
$this->inAppPurchaseUpdater->update($context);
});

return new JsonResponse(status: Response::HTTP_NO_CONTENT);
}

private function getAppByName(string $appName, Context $context): ?AppEntity
{
$criteria = new Criteria();
$criteria->setLimit(1);
$criteria->addFilter(new EqualsFilter('name', $appName));

return $this->appRepository->search($criteria, $context)->getEntities()->first();
}
}
9 changes: 9 additions & 0 deletions src/Exception/ExtensionStoreException.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,13 @@ public static function invalidExtensionCart(string $errorMessage): self
['errorMessage' => $errorMessage],
);
}

public static function invalidInAppPurchase(): self
{
return new self(
Response::HTTP_BAD_REQUEST,
'FRAMEWORK__INVALID_IN_APP_PURCHASE',
'The in-app purchase could not be completed. Please contact the extension provider.',
);
}
}
Loading

0 comments on commit 362dd7b

Please sign in to comment.