From ebe6c148e81b68d5340dc913bc7b464ea041a7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Wed, 22 Oct 2025 13:40:29 +0200 Subject: [PATCH 01/38] ECTO-2233 - added extenison-store-channel service to handle communication with the iframe --- .../app/administration/package-lock.json | 51 +- src/Resources/app/administration/package.json | 3 + .../src/module/sw-extension-store/index.js | 66 +- .../page/sw-extension-store-detail/index.js | 448 ------------- .../sw-extension-store-detail.html.twig | 594 ----------------- .../sw-extension-store-detail.scss | 339 ---------- .../sw-extension-store-detail.spec.js | 614 ------------------ .../page/sw-extension-store-index/index.js | 109 +--- .../sw-extension-store-index.html.twig | 82 +-- .../sw-extension-store-index.spec.js | 173 +---- .../page/sw-extension-store-listing/index.js | 106 --- .../sw-extension-store-listing.html.twig | 28 - .../sw-extension-store-listing.scss | 50 -- .../extension-store-channel.service.ts | 156 +++++ .../app/administration/tsconfig.json | 7 +- 15 files changed, 244 insertions(+), 2582 deletions(-) delete mode 100644 src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/index.js delete mode 100644 src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.html.twig delete mode 100644 src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.spec.js delete mode 100644 src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/index.js delete mode 100644 src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.html.twig create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts diff --git a/src/Resources/app/administration/package-lock.json b/src/Resources/app/administration/package-lock.json index c6676fa8..9db0d247 100644 --- a/src/Resources/app/administration/package-lock.json +++ b/src/Resources/app/administration/package-lock.json @@ -8,6 +8,9 @@ "name": "swag-extension-store", "version": "0.0.1", "license": "MIT", + "dependencies": { + "@shopware-ag/meteor-admin-sdk": "^6.4.0" + }, "devDependencies": { "@babel/preset-env": "^7.20.2", "@shopware-ag/eslint-config-base": "2.0.0", @@ -3776,6 +3779,29 @@ "npm": ">=10.0.0" } }, + "node_modules/@shopware-ag/meteor-admin-sdk": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@shopware-ag/meteor-admin-sdk/-/meteor-admin-sdk-6.4.0.tgz", + "integrity": "sha512-Pyj6/LAhFNqlG5BWHHQRoyYignUTLCFZrK0+XTGJ5XRr2eQP1MhZg8Kn0zxnK26OclL4a23x2dDgaIki73AOig==", + "license": "MIT", + "dependencies": { + "localforage": "^1.10.0", + "lodash": "^4.17.21", + "semver": "^7.7.1" + } + }, + "node_modules/@shopware-ag/meteor-admin-sdk/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7161,6 +7187,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10105,6 +10137,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10112,6 +10153,15 @@ "dev": true, "license": "MIT" }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -10129,7 +10179,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { diff --git a/src/Resources/app/administration/package.json b/src/Resources/app/administration/package.json index 909378d2..639e5229 100644 --- a/src/Resources/app/administration/package.json +++ b/src/Resources/app/administration/package.json @@ -30,5 +30,8 @@ }, "optionalDependencies": { "administration": "^6.x" + }, + "dependencies": { + "@shopware-ag/meteor-admin-sdk": "^6.4.0" } } diff --git a/src/Resources/app/administration/src/module/sw-extension-store/index.js b/src/Resources/app/administration/src/module/sw-extension-store/index.js index 8b3eb4ea..e3ae5658 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/index.js @@ -1,11 +1,12 @@ import ExtensionStoreService from './service/extension-store.service'; import ExtensionStoreDataService from './service/extension-store-data.service'; import ExtensionLicenseService from './service/extension-store-licenses.service'; +import { + ExtensionStoreChannelService, +} from "SwagExtensionStore/module/sw-extension-store/service/extension-store-channel.service"; Shopware.Component.register('sw-extension-store-index', () => import('./page/sw-extension-store-index')); -Shopware.Component.register('sw-extension-store-listing', () => import('./page/sw-extension-store-listing')); -Shopware.Component.register('sw-extension-store-detail', () => import('./page/sw-extension-store-detail')); Shopware.Component.register('sw-extension-store-slider', () => import('./component/sw-extension-store-slider')); Shopware.Component.register('sw-extension-store-listing-filter', () => import('./component/sw-extension-store-listing-filter')); Shopware.Component.register('sw-extension-buy-modal', () => import('./component/sw-extension-buy-modal')); @@ -27,6 +28,15 @@ Shopware.Application.addServiceProvider('extensionStoreService', () => { ); }); +Shopware.Application.addServiceProvider('extensionStoreChannelService', () => { + return new ExtensionStoreChannelService( + Shopware.Service('extensionStoreActionService'), + Shopware.Service('shopwareExtensionService'), + Shopware.Service('extensionStoreLicensesService'), + Shopware.Application.view.router, + ); +}); + Shopware.Application.addServiceProvider('extensionStoreDataService', () => { return new ExtensionStoreDataService( Shopware.Application.getContainer('init').httpClient, @@ -48,61 +58,11 @@ Shopware.Module.register('sw-extension-store', { routePrefixPath: 'sw/extension', routes: { store: { - path: 'store', - redirect: { - name: 'sw.extension.store.listing', - }, + path: 'store*', meta: { privilege: 'system.extension_store', }, component: 'sw-extension-store-index', - children: { - listing: { - path: 'listing', - component: 'sw-extension-store-listing', - redirect: { - name: 'sw.extension.store.listing.app', - }, - meta: { - privilege: 'system.extension_store', - }, - children: { - app: { - path: 'app', - component: 'sw-extension-store-listing', - propsData: { - isTheme: false, - }, - meta: { - privilege: 'system.extension_store', - }, - }, - theme: { - path: 'theme', - component: 'sw-extension-store-listing', - propsData: { - isTheme: true, - }, - meta: { - privilege: 'system.extension_store', - }, - }, - }, - }, - }, - }, - 'store.detail': { - component: 'sw-extension-store-detail', - path: 'store/detail/:id', - meta: { - parentPath: 'sw.extension.store', - privilege: 'system.extension_store', - }, - props: { - default: (route) => { - return { id: route.params.id }; - }, - }, }, }, diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/index.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/index.js deleted file mode 100644 index 397db12c..00000000 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/index.js +++ /dev/null @@ -1,448 +0,0 @@ -import template from './sw-extension-store-detail.html.twig'; -import './sw-extension-store-detail.scss'; - -const { Utils } = Shopware; - -/** - * @private - */ -export default { - template, - - inject: [ - 'extensionStoreService', - 'extensionStoreDataService', - 'shopwareExtensionService', - 'extensionHelperService', - 'cacheApiService', - 'inAppPurchasesService', - ], - - mixins: ['sw-extension-error'], - - props: { - id: { - type: String, - required: true, - }, - }, - - data() { - return { - extension: null, - inAppPurchases: [], - isLoading: false, - showBuyModal: false, - showPermissionsModal: false, - showAcceptPermissionsModal: false, - showAccountModal: false, - showInstallationFailedModal: false, - showInAppPurchasesListingModal: false, - isInstalling: false, - isInstallSuccessful: false, - permissionsAccepted: false, - isDescriptionCollapsed: false, - installationError: null, - fetchError: false, - canBeOpened: false, - }; - }, - - computed: { - suspended() { - return this.extension === null; - }, - - calculatedPrice() { - return this.extensionStoreService.getCalculatedPrice(this.recommendedVariant); - }, - - calculatedPriceSnippet() { - return this.extensionStoreService.getCalculatedPriceSnippet(this.extension.variants); - }, - - extensionMetaData() { - if (this.suspended) { - return null; - } - - return Shopware.Store.get('shopwareExtensions').myExtensions.data.find((extension) => { - return extension.id === this.extension.id; - }); - }, - - isFree() { - return this.extensionStoreService.isVariantOfTypeFree(this.recommendedVariant); - }, - - isLicensed() { - if (!this.extensionMetaData) { - return false; - } - - return !!this.extensionMetaData.storeLicense; - }, - - isInstalled() { - return !!this.extensionMetaData && !!this.extensionMetaData.installedAt; - }, - - isConfigurable() { - return !!this.extensionMetaData && this.extensionMetaData.configurable; - }, - - images() { - if (this.suspended) { - return []; - } - - return this.extension.images.map((image) => { - return image.remoteLink; - }); - }, - - extensionCategoryNames() { - if (this.suspended) { - return ''; - } - - return this.extension.categories.map((category) => category.details.name).join(', '); - }, - - extensionLanguages() { - if (this.suspended) { - return ''; - } - - return this.extension.languages.join(', '); - }, - - isPurchasable() { - if (this.suspended) { - return false; - } - - if (!this.extension.variants.length) { - return false; - } - - return !this.isLicensed; - }, - - languageId() { - return Shopware.Store.get('session').languageId; - }, - - recommendedVariant() { - return this.extensionStoreService.getRecommendedVariant(this.extension.variants); - }, - - dateFilter() { - return Shopware.Filter.getByName('date'); - }, - - dateFilterOptions() { - return { - month: 'numeric', - year: 'numeric', - }; - }, - - hasActiveDiscount() { - return this.extensionStoreService.isExtensionDiscounted(this.extension.variants); - }, - - variantClass() { - return { - 'is--theme': this.extension && this.extension.isTheme, - }; - }, - - orderedBinaries() { - return Utils.get(this.extension, 'binaries', []).slice().reverse(); - }, - - priceClass() { - return { - 'sw-extension-store-detail__price-discounted': this.hasActiveDiscount, - }; - }, - - description() { - return Utils.get(this.extension, 'description'); - }, - - hasPermissions() { - return Object.keys(this.extension.permissions).length; - }, - - installationErrorDocumentationLink() { - return Utils.get(this.installationError, 'meta.documentationLink', null); - }, - - isEnterpriseFeature() { - if (this.suspended) { - return false; - } - - return !!this.extension.addons.find(addon => addon === 'SW6_EnterpriseFeature'); - }, - }, - - watch: { - id: { - immediate: true, - handler() { - this.fetchExtension(); - }, - }, - - '$route.hash'() { - this.scrollToElementFromHash(); - }, - - suspended() { - if (!this.suspended) { - // wait for all child components to be mounted - this.$nextTick(() => { - this.scrollToElementFromHash(); - }); - } - }, - - languageId(newValue) { - if (newValue !== '') { - this.fetchExtension(); - } - }, - - description() { - this.isDescriptionCollapsed = true; - this.$nextTick(() => { - this.checkDescriptionCollapsed(); - }); - }, - }, - - async created() { - await this.shopwareExtensionService.updateExtensionData(); - this.canBeOpened = !!this.shopwareExtensionService.getOpenLink(this.extension); - if (this.extension.inAppFeaturesAvailable) { - this.fetchInAppPurchases(); - } - }, - - methods: { - async fetchExtension() { - this.isLoading = true; - - if (this.languageId === '') { - return; - } - - try { - this.fetchError = false; - this.extension = await this.extensionStoreDataService.getDetail( - this.id, - { ...Shopware.Context.api, languageId: this.languageId }, - ); - } catch (error) { - const errorData = error.response.data.errors[0]; - - if (errorData.code === 'FRAMEWORK__STORE_ERROR' && errorData.title === 'Extension unknown') { - this.fetchError = errorData; - - - const docLink = this.$t('sw-extension.errors.messageToTheShopwareDocumentation', errorData.meta); - - this.createNotificationError({ - title: errorData.title, - message: `${errorData.detail}
${docLink}`, - autoClose: false, - }); - - return; - } - - this.showExtensionErrors(error); - } finally { - this.isLoading = false; - } - }, - - async fetchExtensionAndScrollToRatings() { - await this.fetchExtension(); - this.$router.push({ hash: '#ratings-card' }); - }, - - scrollToElementFromHash() { - if (!this.$route.hash) { - return; - } - - const elementWithHashId = this.$el.querySelector(this.$route.hash); - const pageBody = document.querySelector('div.sw-meteor-page__body'); - const header = document.querySelector('header.sw-meteor-page__head-area'); - - const topOfElement = elementWithHashId.offsetTop - header.getBoundingClientRect().height; - - pageBody.scroll({ top: topOfElement, behavior: 'smooth' }); - - // clear hash so user can click link again - this.$router.push({ ...this.route, hash: null }); - }, - - async onClickAddExtension() { - await this.shopwareExtensionService.checkLogin(); - - if (!Shopware.Store.get('shopwareExtensions').userInfo) { - this.openAccountModal(); - return; - } - - this.openBuyModal(); - }, - - renderPrice(price) { - return Utils.format.currency(price, 'EUR'); - }, - - onLoginSuccess() { - this.closeAccountModal(); - this.openBuyModal(); - }, - - openBuyModal() { - this.showBuyModal = true; - }, - - closeBuyModal() { - this.showBuyModal = false; - }, - - openAccountModal() { - this.showAccountModal = true; - }, - - closeAccountModal() { - this.showAccountModal = false; - }, - - openPermissionsModal() { - this.showPermissionsModal = true; - }, - - closePermissionsModal() { - this.showPermissionsModal = false; - }, - - openAcceptPermissionsModal() { - this.showAcceptPermissionsModal = true; - }, - - closeAcceptPermissionsModal() { - this.showAcceptPermissionsModal = false; - }, - - openInAppPurchasesListingModal() { - this.showInAppPurchasesListingModal = true; - }, - - closeInAppPurchasesListingModal() { - this.showInAppPurchasesListingModal = false; - }, - - async closePermissionsModalAndInstallExtension() { - this.permissionsAccepted = true; - this.closeAcceptPermissionsModal(); - await this.installExtension(); - }, - - async handleInstallWithPermissionsModal() { - if (Object.keys(this.extension.permissions).length) { - this.openAcceptPermissionsModal(); - - return; - } - - this.permissionsAccepted = true; - await this.installExtension(); - }, - - async installExtension() { - this.isInstalling = true; - - try { - await this.extensionHelperService.downloadAndActivateExtension(this.extension.name, this.extension.type); - - if (this.extension.type === 'plugin') { - await this.cacheApiService.clear(); - } - - this.isInstallSuccessful = true; - - window.location.reload(); - } catch (error) { - this.showExtensionErrors(error); - - if (Utils.get(error, 'response.data.errors[0]', null)) { - this.installationError = error.response.data.errors[0]; - } - - this.openInstallationFailedModal(); - } finally { - this.isInstalling = false; - } - }, - - async finishedInstall() { - await this.shopwareExtensionService.updateExtensionData(); - this.isInstallSuccessful = false; - }, - - async openExtension() { - const openLink = await this.shopwareExtensionService.getOpenLink(this.extension); - - if (openLink) { - this.$router.push(openLink); - } - }, - - openConfiguration() { - this.$router.push({ name: 'sw.extension.config', params: { namespace: this.extension.name } }); - }, - - openListingPage() { - if (this.extension && this.extension.isTheme) { - this.$router.push({ name: 'sw.extension.my-extensions.listing.theme' }); - return; - } - - this.$router.push({ name: 'sw.extension.my-extensions.listing.app' }); - }, - - checkDescriptionCollapsed() { - const description = this.$el.querySelector('.sw-extension-store-detail__description'); - - if (description && description.scrollHeight <= 300) { - this.expandDescription(); - } - }, - - expandDescription() { - this.isDescriptionCollapsed = false; - }, - - openInstallationFailedModal() { - this.showInstallationFailedModal = true; - }, - - closeInstallationFailedModal() { - this.showInstallationFailedModal = false; - }, - - async fetchInAppPurchases() { - this.inAppPurchases = await this.inAppPurchasesService.getAvailablePurchases(this.extension.name); - }, - }, -}; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.html.twig deleted file mode 100644 index a23bf3a4..00000000 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.html.twig +++ /dev/null @@ -1,594 +0,0 @@ -{% block sw_extension_store_detail %} - -
- - - -

{{ fetchError.title }}

-

{{ fetchError.detail }}

-
-
- - - {% block sw_extension_store_detail_loader %}{% endblock %} - - - - {% block sw_extension_store_detail_smart_bar %} - {% block sw_extension_store_detail_slot_icon %} - - {% endblock %} - - {% block sw_extension_store_detail_slot_smart_bar_header %} - - {% endblock %} - - {% block sw_extension_store_detail_slot_smart_bar_header_meta %} - - {% endblock %} - - {% block sw_extension_store_detail_slot_smart_bar_description %} - - {% endblock %} - - {% block sw_extension_store_detail_slot_smart_bar_actions %} - - {% endblock %} - - {% block sw_extension_store_detail_slot_smart_bar_context_buttons %} - - {% endblock %} - {% endblock %} - - {% block sw_extension_store_detail_page_content %} - {% block sw_extension_sote_detail_page_info_alerts %} - - {{ $t('sw-extension-store.detail.enterpriseFeatureAlertText') }} - - {% endblock %} - - {% block sw_extension_store_detail_images %} - - {% endblock %} - - {% block sw_extension_store_detail_card_info %} - - {% block sw_extension_store_detail_card_info_content %} -
- {% endblock %} - - {% block sw_extension_store_details_expand_description %} - - {{ $t('sw-extension-store.detail.labelButtonReadMore') }} - - {% endblock %} - - {% endblock %} - - {% block sw_extension_store_detail_card_details %} - - {% block sw_extension_store_detail_card_details_content %} -
- {% block sw_extension_store_detail_card_details_category %} -
{{ $t('sw-extension-store.detail.details.labelCategory', extension.categories.length) }}
-
{{ extensionCategoryNames }}
- {% endblock %} - - {% block sw_extension_store_detail_card_details_version %} -
{{ $t('sw-extension-store.detail.details.labelVersion') }}
-
{{ extension.latestVersion }}
- {% endblock %} - - {% block sw_extension_store_detail_card_details_updated_at %} -
{{ $t('sw-extension-store.detail.details.labelUpdatedAt') }}
-
{{ dateFilter(extension.lastUpdateDate, { month: 'numeric', year: 'numeric', hour: undefined, minute: undefined }) }}
- {% endblock %} - - {% block sw_extension_store_detail_card_details_languages %} -
- {{ $t('sw-extension-store.detail.details.labelLanguages') }} -
-
{{ extensionLanguages }}
- {% endblock %} - - {% block sw_extension_store_detail_card_details_in_app_purchases %} - - {% endblock %} - - {% block sw_extension_store_detail_card_details_producer_website %} - - {% endblock %} - - {% block sw_extension_store_detail_card_details_privacy_policy %} - - {% endblock %} - - {% block sw_extension_store_detail_card_details_additional_entries %}{% endblock %} -
- {% endblock %} -
- {% endblock %} - - {% block sw_extension_store_detail_card_ratings %} - - {% endblock %} - - {% block sw_extension_store_detail_card_faq %} - - {% block sw_extension_store_detail_card_faq_questions %} -
- {% block sw_extension_store_detail_card_faq_questions_question %} -

{{ question.question }}

- {% endblock %} - - {% block sw_extension_store_detail_card_faq_questions_answer %} -

- {% endblock %} -

- {% endblock %} -
- {% endblock %} - - {% block sw_extension_store_detail_card_changelog %} - - {% block sw_extension_store_detail_card_changelog_entries %} -
- {% block sw_extension_store_detail_card_changelog_entries_version %} -
- {{ binary.version }} -
- {% endblock %} - - {% block sw_extension_store_detail_card_changelog_entries_creation_date %} -
- {{ dateFilter(binary.creationDate, { month: 'numeric', year: 'numeric', hour: undefined, minute: undefined }) }} -
- {% endblock %} - - {% block sw_extension_store_detail_card_changelog_entries_changelog_text %} -

- {% endblock %} -

- {% endblock %} -
- {% endblock %} - {% endblock %} - - {% block sw_extension_store_detail_page_modals %} - {% block sw_extension_store_detail_page_permission %} - - {% endblock %} - - {% block sw_extension_store_detail_page_permission_accept %} - - {% endblock %} - - {% block sw_extension_store_detail_page_account_modal %} - - {% endblock %} - {% endblock %} - -{% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.scss b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.scss index cd029ac2..e69de29b 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.scss +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.scss @@ -1,339 +0,0 @@ -@import "~scss/variables"; - -.sw-extension-store-detail__extension-unknown { - width: 960px; - max-width: 100%; - height: 450px; - background-color: var(--color-elevation-surface-raised); - margin: 72px auto 0 auto; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - text-align: center; - - .sw-label { - width: 72px; - height: 72px; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 20px; - - .sw-label__caption { - width: auto; - } - } - - h2 { - font-size: $font-size-m; - } - - p { - color: var(--color-text-secondary-default); - font-size: $font-size-xs; - max-width: 325px; - margin-bottom: 1.5rem; - } -} - -.sw-meteor-page.sw-extension-store-detail { - .is--wrap-content { - word-break: break-word; - } - - .sw-extension-store-detail--user-provided-data { - h2, - h3, - h4, - h5, - h6 { - margin-top: 1.5em; - margin-bottom: 1em; - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } - } - - ul, - ol { - padding-left: 16px; - } - - a { - color: var(--color-text-brand-default); - } - } - - .sw-extension-icon { - width: 72px; - height: 72px; - } - - .sw-meteor-page__head-area { - padding-bottom: 12px; - - .sw-meteor-page__smart-bar-module-info { - grid-template-rows: min-content auto; - - .sw-meteor-page__smart-bar-header { - flex-wrap: nowrap; - flex-direction: column; - align-items: stretch; - - .sw-meteor-page__smart-bar-title { - font-size: $font-size-l; - font-weight: $font-weight-bold; - margin-bottom: 5px; - margin-top: -4px; - } - - .sw-meteor-page__smart-bar-meta { - color: var(--color-text-secondary-default); - font-size: $font-size-xxs; - display: flex; - flex-wrap: wrap; - align-items: baseline; - margin-bottom: 1px; - font-weight: $font-weight-medium; - gap: 8px; - } - } - - .sw-meteor-page__smart-bar-description { - color: var(--color-text-secondary-default); - font-size: $font-size-xs; - - .sw-extension-store-detail__label-and-rating-overview { - display: flex; - gap: 8px; - - .sw-extension-store-detail__rating-overview { - flex-shrink: 0; - align-self: start; - display: flex; - align-items: center; - - & > *:not(:last-child) { - margin-right: 8px; - } - - a, - a:active, - a:visited { - font-size: $font-size-xxs; - font-weight: $font-weight-medium; - color: var(--color-text-brand-default); - margin-top: 2px; - transition: color .2s ease; - - &:hover, - &:focus, - &:focus-visible, - &:focus-within { - color: var(--color-text-brand-hover); - } - } - - .sw-extension-store-detail__no-ratings-text { - font-size: $font-size-xxs; - font-weight: $font-weight-medium; - } - } - } - } - } - } - - .sw-extension-store-detail__extension-type-label, - .sw-extension-store-detail__label-display { - margin-right: 4px; - } - - .sw-extension-type-label__cloud-label { - background-color: var(--color-background-tertiary-default) !important; - } - - .sw-extension-store-detail__producer-link { - text-decoration: none; - font-weight: $font-weight-medium; - } - - .sw-meteor-page__smart-bar-actions { - .sw-extension-store-detail__trial-info { - font-size: $font-size-xs; - font-weight: $font-weight-regular; - color: var(--color-text-brand-default); - padding: 0; - border: none; - background: none; - line-height: inherit; - text-align: right; - margin: 0 0 10px 0; - } - - .sw-extension-store-detail__price { - color: var(--color-text-secondary-default); - - strong { - color: var(--color-text-primary-default); - } - - &-badge-sale { - background-color: var(--color-background-critical-default); - border-radius: $border-radius-pill; - color: var(--color-text-critical-default); - display: inline-block; - font-size: $font-size-xxs; - font-weight: $font-weight-medium; - height: $line-height-xs; - line-height: $line-height-xs; - margin-right: 4px; - padding: 0 5px; - vertical-align: bottom; - } - - &-discounted { - strong { - color: var(--color-text-critical-default); - } - } - } - - .sw-extension-store-detail__in-app-purchases__badge { - margin: 0; - font-size: 14px; - } - - .sw-extension-store-detail__price-free { - margin-bottom: 0; - } - - .sw-extension-store-detail__discounted-price-info { - color: var(--color-text-secondary-default); - font-size: $font-size-xxs; - margin-top: 0; - text-align: right; - } - } - - - &.is--theme .sw-extension-store-slider .sw-extension-store-slider__slides { - height: 70vw; - max-height: 720px; - } - - .sw-extension-store-detail__description { - &--collapsed { - max-height: 300px; - overflow-y: hidden; - } - } - - .sw-extension-store-detail__button-extend-description { - margin-top: 16px; - } - - .sw-extension-store-detail__details-list { - display: grid; - grid-template-columns: auto 1fr; - justify-items: start; - column-gap: 40px; - row-gap: 8px; - - dt { - font-weight: $font-weight-semi-bold; - - a { - font-weight: $font-weight-regular; - } - } - } - - .sw-extension-store-detail__faq-question h4 { - margin-bottom: 5px; - } - - .sw-extension-store-detail__faq-question:not(:last-child) { - margin-bottom: 1.5em; - } - - .sw-extension-store-detail__changelog-entry { - display: grid; - grid-template-columns: 96px 1fr; - grid-template-rows: auto 1fr; - column-gap: 40px; - grid-template-areas: - "version changelog" - "date changelog"; - } - - .sw-extension-store-detail__changelog-entry:not(:last-child) { - margin-bottom: 32px; - padding-bottom: 32px; - border-bottom: 1px dashed var(--color-border-primary-default); - } - - .sw-extension-store-detail__changelog-version { - grid-area: version; - justify-self: right; - text-align: right; - font-size: $font-size-s; - font-weight: $font-weight-bold; - } - - .sw-extension-store-detail__changelog-creation-date { - grid-area: date; - justify-self: right; - text-align: right; - font-size: $font-size-xxs; - font-weight: $font-weight-medium; - color: var(--color-text-secondary-default); - } - - .sw-extension-store-detail__changelog-text { - grid-area: changelog; - font-size: $font-size-xs; - p, ul, ol { - margin-bottom: 12px; - li:not(:last-child) { - margin-bottom: 8px; - } - } - ul { - list-style: none; - li { - margin-left: 16px; - position: relative; - &:before{ - content: "\2022"; - position: absolute; - left: -16px; - } - } - } - ol { - list-style-position: inside; - } - } - - .sw-extension-store-detail__alert { - max-width: 960px; - margin: 0 auto 40px auto; - } -} - -.sw-extension-store-detail__account-modal { - .sw-modal__body { - padding: 0; - } - - .sw-meteor-card { - margin: 0; - } -} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.spec.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.spec.js deleted file mode 100644 index ebc368f3..00000000 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-detail/sw-extension-store-detail.spec.js +++ /dev/null @@ -1,614 +0,0 @@ -import { mount } from '@vue/test-utils'; -import ShopwareExtensionService from 'src/module/sw-extension/service/shopware-extension.service'; -import ShopwareDiscountCampaignService from 'src/app/service/discount-campaign.service'; -import ExtensionStoreService from 'SwagExtensionStore/module/sw-extension-store/service/extension-store.service'; - -Shopware.Component.register( - 'sw-extension-store-detail', - () => import('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-detail'), -); - -const cacheApiService = { - clear: jest.fn(() => Promise.resolve()), -}; - -const extensionHelperService = { - downloadAndActivateExtension: jest.fn(() => Promise.resolve()), -}; - -async function createWrapper(extensionCustomProps = {}, canBeOpened = true, inAppPurchases = true) { - const testExtension = { - id: 1337, - categories: [ - { details: { name: 'Productivity' } }, - { details: { name: 'Admin' } }, - { details: { name: 'Storefront' } }, - ], - description: '

This is a really cool extension.

', - inAppFeaturesAvailable: inAppPurchases, - label: 'B2B Suite', - name: 'SwagB2BPlatform', - producerName: 'shopware AG', - shortDescription: 'Foo', - type: 'plugin', - updateSource: 'local', - variants: [{}], - languages: ['German', 'English'], - storeLicense: { variants: [{}] }, - images: [], - permissions: [], - labels: [], - faq: [], - addons: [], - ...extensionCustomProps, - }; - - const inAppPurchasesService = getInAppPurchasesMockService(inAppPurchases); - - return mount(await Shopware.Component.build('sw-extension-store-detail'), { - props: { - id: 'a1b2c3', - }, - global: { - renderStubDefaultSlot: true, - stubs: { - /* sw-meteor-page */ - 'sw-meteor-page': await wrapTestComponent('sw-meteor-page'), - 'sw-notification-center': true, - 'sw-help-center-v2': true, - 'sw-meteor-page-context': true, - 'sw-meteor-navigation': true, - 'sw-tabs': true, - 'sw-extension-component-section': true, - 'sw-app-topbar-button': true, - 'sw-search-bar': { - template: '', - }, - /* sw-meteor-page */ - - 'sw-loader': true, - 'sw-extension-type-label': true, - 'sw-extension-store-slider': true, - 'sw-meteor-card': true, - 'sw-label': await wrapTestComponent('sw-label'), - 'sw-button-group': true, - 'sw-context-button': true, - 'sw-context-menu-item': true, - 'sw-extension-ratings-card': true, - 'sw-button-process': await wrapTestComponent('sw-button-process'), - 'sw-extension-rating-stars': true, - 'router-link': { - template: '', - }, - 'sw-extension-store-label-display': true, - 'sw-extension-buy-modal': true, - 'sw-extension-permissions-modal': true, - 'sw-extension-my-extensions-account': true, - 'sw-extension-adding-failed': true, - 'sw-extension-store-in-app-purchases-listing-modal': true, - 'sw-extension-icon': true, - 'sw-external-link': true, - 'sw-app-topbar-sidebar': true, - }, - provide: { - shopwareExtensionService: { - updateExtensionData: jest.fn(), - isVariantDiscounted: jest.fn(), - orderVariantsByRecommendation: () => [], - getOpenLink: () => (canBeOpened ? 'open-link' : null), - }, - extensionStoreService: new ExtensionStoreService( - new ShopwareDiscountCampaignService(), - new ShopwareExtensionService( - undefined, - undefined, - new ShopwareDiscountCampaignService(), - ), - ), - extensionStoreDataService: { - getDetail: () => { - return testExtension; - }, - }, - extensionHelperService, - cacheApiService, - inAppPurchasesService, - }, - }, - }); -} - -const setSearchValueMock = jest.fn(); -describe('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-detail', () => { - const originalWindowLocation = window.location; - - beforeAll(async () => { - Shopware.Store.register({ - id: 'shopwareExtensions', - actions: { - setSearchValue: setSearchValueMock, - }, - }); - - Object.defineProperty(window, 'location', { - configurable: true, - value: { reload: jest.fn() }, - }); - }); - - beforeEach(() => { - Shopware.Store.get('session').languageId = 'b2c3d4'; - Shopware.Store.get('shopwareExtensions').myExtensions = { - loading: false, - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: { variants: [{}] }, - id: 1337, - }], - }; - - setSearchValueMock.mockClear(); - }); - - afterEach(() => { - Shopware.Store.get('session').languageId = ''; - Shopware.Store.get('shopwareExtensions').myExtensions = { data: [], loading: false }; - }); - - afterAll(() => { - Object.defineProperty(window, 'location', { configurable: true, value: originalWindowLocation }); - }); - - it('should show all extension category names', async () => { - const wrapper = await createWrapper(); - - expect(wrapper.vm.extensionCategoryNames).toBe('Productivity, Admin, Storefront'); - }); - - it('should render alert box when extension is an enterprise feature', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: false, - id: 1337, - }], - }; - - const wrapper = await createWrapper({ - storeLicense: false, - addons: ['SW6_EnterpriseFeature'], - variants: [], - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__alert').text()) - .toBe('sw-extension-store.detail.enterpriseFeatureAlertText'); - }); - - it('should clear cache and reload the administration when a plugin is installed', async () => { - const wrapper = await createWrapper(); - await flushPromises(); - - const installButton = await wrapper.get('.sw-extension-store-detail__action-install-extension'); - await installButton.trigger('click'); - await flushPromises(); - - expect(extensionHelperService.downloadAndActivateExtension).toHaveBeenCalledTimes(1); - expect(cacheApiService.clear).toHaveBeenCalledTimes(1); - expect(window.location.reload).toHaveBeenCalledTimes(1); - }); - - it('should reload the administration when an app is installed', async () => { - const wrapper = await createWrapper({ type: 'app' }); - await flushPromises(); - - const installButton = await wrapper.get('.sw-extension-store-detail__action-install-extension'); - await installButton.trigger('click'); - await flushPromises(); - - expect(extensionHelperService.downloadAndActivateExtension).toHaveBeenCalledTimes(1); - expect(cacheApiService.clear).not.toHaveBeenCalled(); - expect(window.location.reload).toHaveBeenCalledTimes(1); - }); - - describe('verify smart bar price', () => { - beforeEach(() => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SomeOtherExtension', - storeLicense: { variants: [{}] }, - id: 555, - }], - }; - }); - - it('should render "free" when extension is not licensed and free', async () => { - const wrapper = await createWrapper({ - variants: [{ type: 'free' }], - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__price-free').text()) - .toBe('sw-extension-store.general.labelFree'); - }); - - it('should render monthly price when extension is not licensed', async () => { - const wrapper = await createWrapper({ - variants: [{ - type: 'rent', - netPrice: 10, - netPricePerMonth: 10, - duration: 1, - }], - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__price').text()) - .toContain('sw-extension-store.general.labelPricePerMonth'); - }); - - it('should render monthly from price when extension is not licensed', async () => { - const wrapper = await createWrapper({ - variants: [{ - type: 'rent', - netPrice: 10, - netPricePerMonth: 10, - duration: 1, - }, { - type: 'rent', - netPrice: 96, - netPricePerMonth: 8, - duration: 12, - }], - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__price').text()) - .toContain('sw-extension-store.general.labelFromPricePerMonth'); - }); - - it('should render sale badge when extension is not licensed and has active discount', async () => { - const wrapper = await createWrapper({ - variants: [{ - type: 'rent', - netPrice: 10, - netPricePerMonth: 10, - duration: 1, - discountCampaign: { - discountedPrice: 6, - discountedPricePerMonth: 6, - discountAppliesForMonths: 3, - startDate: '2021-01-27T00:01:00+01:00', - endDate: '2121-01-28T00:01:00+01:00', - }, - }, { - type: 'rent', - netPrice: 96, - netPricePerMonth: 8, - duration: 12, - }], - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__price').text()) - .toContain('sw-extension-store.general.labelSale'); - }); - }); - - describe('verify smart bar primary action buttons', () => { - it('should render "add extension" button when extension is purchasable', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'TestExtension', - storeLicense: false, - id: 1337, - }], - }; - - const wrapper = await createWrapper({ - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-add-extension').text()) - .toBe('sw-extension-store.detail.labelButtonAddExtension'); - }); - - it('should render "install" button when extension is not installed but already licensed', async () => { - const wrapper = await createWrapper(); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-install-extension').text()) - .toBe('sw-extension-store.detail.labelButtonInstallExtension'); - }); - - it('should render "open" button when extension is installed and licensed', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: { variants: [{}] }, - id: 1337, - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }], - }; - - const wrapper = await createWrapper({ - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-open-extension').text()) - .toBe('sw-extension-store.detail.labelButtonOpenExtension'); - }); - - - it('should render "configuration" context menu when extension is installed, licensed and configurable', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: { variants: [{}] }, - id: 1337, - configurable: true, - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }], - }; - - const wrapper = await createWrapper({ - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-context').text()) - .toBe('sw-extension-store.detail.openConfiguration'); - }); - - - it('should render "configuration" button when extension is installed, licensed and configurable but can\'t be opened', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: { variants: [{}] }, - configurable: true, - id: 1337, - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }], - }; - - const wrapper = await createWrapper({ - installedAt: { - date: '2021-07-08 07:34:11.794000', - timezone: 'UTC', - timezone_type: 3, - }, - }, false); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-open-configuration').text()) - .toBe('sw-extension-store.detail.openConfiguration'); - }); - - it('should render "contact us" button when extension is not licensed and has enterprise flag', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SwagB2BPlatform', - storeLicense: null, - id: 1337, - }], - }; - - const wrapper = await createWrapper({ - storeLicense: null, - addons: ['SW6_EnterpriseFeature'], - variants: [], - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__action-enterprise-contact').text()) - .toBe('sw-extension-store.detail.enterpriseContactLinkText'); - }); - - it('should render "free" when extension is not licensed and free', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SomeOtherExtension', - storeLicense: { variants: [{}] }, - id: 555, - }], - }; - - const wrapper = await createWrapper({ - variants: [{ type: 'free' }], - storeLicense: null, - }); - await flushPromises(); - - expect(wrapper.get('.sw-extension-store-detail__price-free').text()) - .toBe('sw-extension-store.general.labelFree'); - }); - - it('should not render any button when extension is not licensed, not purchasable and has no enterprise flag', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'SomeOtherExtension', - storeLicense: { variants: [{}] }, - id: 555, - }], - }; - - const wrapper = await createWrapper({ - storeLicense: null, - variants: [], - }); - await flushPromises(); - - expect(wrapper.find('.sw-extension-store-detail__action-add-extension').exists()).toBeFalsy(); - expect(wrapper.find('.sw-extension-store-detail__action-open-extension').exists()).toBeFalsy(); - expect(wrapper.find('.sw-extension-store-detail__action-install-extension').exists()).toBeFalsy(); - expect(wrapper.find('.sw-extension-store-detail__action-enterprise-contact').exists()).toBeFalsy(); - }); - - it('should render in-app-purchase badge when extension has available in-app-purchase', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'TestExtension', - storeLicense: false, - id: 1337, - }], - }; - - const wrapper = await createWrapper({ - inAppFeaturesAvailable: true, - variants: [{ foo: 'bar' }], - }); - - await flushPromises(); - - expect(wrapper.find('.sw-extension-store-detail__in-app-purchases__badge').exists()).toBeTruthy(); - expect(wrapper.get('.sw-extension-store-detail__in-app-purchases__badge').text()) - .toBe('sw-extension.in-app-purchase.badge-label'); - }); - - it('should not render in-app-purchase badge when extension has available in-app-purchase', async () => { - Shopware.Store.get('shopwareExtensions').myExtensions = { - data: [{ - active: true, - name: 'TestExtension', - storeLicense: false, - id: 1337, - }], - }; - - const wrapper = await createWrapper({ - inAppFeaturesAvailable: false, - variants: [{ foo: 'bar' }], - }); - - await flushPromises(); - - expect(wrapper.find('.sw-extension-store-detail__in-app-purchases__badge').exists()).toBeFalsy(); - }); - - it('should not render in-app purchases section when not available', async () => { - const wrapper = await createWrapper({}, true, false); - await flushPromises(); - - expect(wrapper.find('.sw-extension-store-detail-card-details-in-app-purchases__count').exists()).toBe(false); - }); - - it('should render in-app purchases section when available', async () => { - const wrapper = await createWrapper(); - await flushPromises(); - - expect(wrapper.find('.sw-extension-store-detail-card-details-in-app-purchases__count').exists()).toBe(true); - expect(wrapper.get('.sw-extension-store-detail-card-details-in-app-purchases__count').text()).toBe('2'); - }); - }); -}); - -function getInAppPurchasesMockService(inAppPurchases) { - return { - getAvailablePurchases: jest.fn(() => getInAppPurchaseMockResponse(inAppPurchases)), - }; -} - -function getInAppPurchaseMockResponse(inAppPurchases) { - if (!inAppPurchases) { - return []; - } - return [ - { - identifier: 'feature1', - name: 'Feature One', - description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at magna commodo, sodales ' + - 'mauris ut, aliquam dolor. Proin tellus nunc, tempor eget blandit vel, elementum quis velit. ' + - 'Curabitur bibendum consequat odio, sed aliquam lacus vestibulum in. Nam eleifend sollicitudin ' + - 'lorem, gravida fringilla urna consectetur id. Nullam lacus nunc, fringilla at odio id, luctus porta ' + - 'dolor. Ut varius pretium ex, non porta mi vulputate id. Suspendisse vel maximus turpis, nec lacinia ' + - 'dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis ' + - 'egestas. Ut vel quam placerat, ultricies nunc ac, dapibus urna. Sed tincidunt ullamcorper arcu nec ' + - 'tempus. Aenean elementum augue eget cursus cursus. Donec vitae elementum elit. Nunc ac sem in ante ' + - 'hendrerit vulputate vel id tortor. Curabitur mollis vulputate urna, vel viverra lorem. Nunc sed ' + - 'neque at libero ornare commodo.\n\n' + - 'Etiam fringilla ornare mauris in fermentum. Aliquam dapibus facilisis nisi at porta. Quisque ' + - 'dignissim sem ipsum, in efficitur metus blandit non. Donec sodales eleifend ipsum nec viverra. ' + - 'Etiam vitae ipsum magna. Nam mi ante, dignissim eget malesuada nec, feugiat vel ipsum. Phasellus ' + - 'augue erat, imperdiet non odio a, luctus lacinia est. Nunc blandit purus sit amet ante vehicula ' + - 'accumsan. Duis neque metus, consequat non enim at, porta maximus risus. Duis euismod odio sapien, ' + - 'in feugiat magna ultricies vel. Curabitur dictum condimentum ligula dapibus faucibus.\n\n' + - 'Donec augue risus, vulputate in ipsum nec, gravida eleifend ante. Quisque maximus sapien dui, eget ' + - 'mollis dolor gravida ac. Proin a enim tincidunt, maximus erat ac, fermentum nisi. Nullam molestie ' + - 'eros vel ipsum viverra, sit amet congue nunc feugiat. Nullam elementum lorem libero, in scelerisque ' + - 'nunc congue in. Aliquam in dolor suscipit, pellentesque leo vel, malesuada velit. Ut felis mauris, ' + - 'lacinia ut efficitur ut, consectetur at ipsum. Nam dui magna, fringilla id semper sed, vulputate ' + - 'vel nunc. Duis quis metus ante.\n\n' + - 'Sed at enim eu velit vestibulum efficitur ac non nunc. Vestibulum in maximus ligula, ac condimentum ' + - 'erat. Proin mauris nunc, ullamcorper quis lorem vitae, porta convallis orci. Maecenas vitae lectus ' + - 'mauris. Cras leo nisl, finibus vitae varius vitae, maximus eget erat. In vitae tristique libero. ' + - 'Nam eget eleifend leo.', - priceModels: [ - { - price: 9.99, - currency: 'USD', - duration: 'monthly', - }, - { - price: 99.99, - currency: 'USD', - duration: 'yearly', - }, - ], - }, - { - identifier: 'feature2', - name: 'Feature Two', - description: 'Description for Feature Two', - priceModels: [ - { - price: 4.99, - currency: 'USD', - duration: 'monthly', - }, - { - price: 49.99, - currency: 'USD', - duration: 'yearly', - }, - ], - }, - ]; -} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js index c0efc4dc..a23c859a 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js @@ -7,117 +7,16 @@ import './sw-extension-store-index.scss'; export default { template, - inject: ['extensionStoreActionService', 'shopwareExtensionService', 'feature'], - - props: { - id: { - type: String, - required: false, - default: null, - }, - }, + // we have to add the extensionStoreLicensesService for the checkout example + inject: ['extensionStoreChannelService'], data() { return { - isAvailable: false, - failReason: '', - listingError: null, - isLoading: false, - }; - }, - - computed: { - storeSearchKey() { - return this.$route.name; - }, - - activeFilters() { - return Shopware.Store.get('shopwareExtensions').search.filter; - }, - - searchValue() { - return Shopware.Store.get('shopwareExtensions').search.term; - }, - isTheme() { - const isTheme = this.$route.name.includes('theme'); - - return isTheme ? 'themes' : 'apps'; - }, - }, - - watch: { - isTheme: { - immediate: true, - handler(newValue) { - Shopware.Store.get('shopwareExtensions').setSearchValue({ - key: 'page', - value: this.isTheme === this.activeFilters.group ? Shopware.Store.get('shopwareExtensions').search.page : 1, - }); - this.activeFilters.group = newValue; - }, - }, + }; }, created() { - this.createdComponent(); - }, - - methods: { - createdComponent() { - this.checkStoreUpdates(); - }, - - async checkStoreUpdates() { - this.isLoading = true; - - this.shopwareExtensionService.updateExtensionData(); - - const extensionStore = await this.getExtensionStore(); - - if (!extensionStore) { - this.isLoading = false; - return; - } - - if (this.isUpdateable(extensionStore)) { - this.isAvailable = false; - this.failReason = 'outdated'; - this.isLoading = false; - return; - } - - this.isAvailable = true; - this.isLoading = false; - }, - - onExtensionListingError(e) { - const errors = Shopware.Service('extensionErrorService').handleErrorResponse(e, this); - - this.isAvailable = false; - this.listingError = errors && errors[0]; - this.failReason = 'listing_error'; - }, - - getExtensionStore() { - return this.extensionStoreActionService.getMyExtensions().then((extensions) => { - return extensions.find(extension => extension.name === 'SwagExtensionStore'); - }); - }, - - isUpdateable(extension) { - if (!extension || extension.latestVersion === null) { - return false; - } - - return extension.latestVersion !== extension.version; - }, - - updateSearch(term) { - Shopware.Store.get('shopwareExtensions').setSearchValue({ - key: 'term', - value: term, - }); - }, + this.extensionStoreChannelService.register(); }, }; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig index 5ffca039..d9b07dbf 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig @@ -1,83 +1,3 @@ {% block sw_extension_store_index %} - - {% block sw_extension_store_index_slots %} - {% block sw_extension_store_index_slot_search_bar %} - - {% endblock %} - - {% block sw_extension_store_index_slot_tabs %} - - {% endblock %} - - {% block sw_extension_store_index_slot_default %} - - {% endblock %} - {% endblock %} - + {% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js index 60148ad5..251ad1ee 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js @@ -1,30 +1,17 @@ import { mount } from '@vue/test-utils'; -import ExtensionErrorService from 'src/module/sw-extension/service/extension-error.service'; import 'src/app/component/meteor/sw-meteor-page'; Shopware.Component.register( 'sw-extension-store-index', () => import('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-index'), ); -Shopware.Component.register( - 'sw-extension-store-detail', - () => import('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-detail'), -); - -const myExtensionsMock = jest.fn(() => Promise.resolve([{ - name: 'SwagExtensionStore', - latestVersion: null, - version: '1.3.0', -}])); - -Shopware.Application.addServiceProvider('extensionErrorService', () => { - return new ExtensionErrorService({}, { - title: 'global.default.error', - message: 'global.notification.unspecifiedSaveErrorMessage', - }); -}); -async function createWrapper() { +async function createWrapper(channelService = null) { + if (channelService === null) { + channelService = { + register: jest.fn(), + }; + } return mount(await Shopware.Component.build('sw-extension-store-index'), { props: {}, global: { @@ -38,162 +25,28 @@ async function createWrapper() { }), }, stubs: { - /* sw-meteor-page */ - 'sw-meteor-page': await wrapTestComponent('sw-meteor-page'), - 'sw-notification-center': true, - 'sw-help-center-v2': true, - 'sw-meteor-page-context': true, - 'sw-meteor-navigation': true, - 'sw-tabs': true, - 'sw-extension-component-section': true, - 'sw-app-topbar-button': true, - 'sw-search-bar': { - template: '', - }, - /* sw-meteor-page */ - - 'sw-loader': true, - 'sw-tabs-item': true, - 'router-view': true, - 'sw-extension-store-error-card': true, - 'sw-extension-store-update-warning': true, - 'sw-help-center': true, - 'sw-app-topbar-sidebar': true, + 'sw-iframe-renderer': true, }, provide: { - extensionStoreActionService: { - getMyExtensions: myExtensionsMock, - }, - shopwareExtensionService: { - updateExtensionData: jest.fn(), - }, - extensionErrorService: Shopware.Service('extensionErrorService'), + extensionStoreChannelService: channelService, }, }, }); } -const setSearchValueMock = jest.fn(); describe('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-index', () => { - beforeAll(async () => { - Shopware.Store.register({ - id: 'shopwareExtensions', - state: () => ({ - search: { - filter: {}, - }, - }), - actions: { - setSearchValue: setSearchValueMock, - }, - }); - }); - - beforeEach(async () => { - Shopware.Store.get('shopwareExtensions').search.filter = {}; - setSearchValueMock.mockClear(); - myExtensionsMock.mockClear(); - }); - it('should be a Vue.JS component', async () => { const wrapper = await createWrapper(); expect(wrapper.vm).toBeTruthy(); }); - it('should commit the search value to the store', async () => { - const wrapper = await createWrapper(); - await flushPromises(); - - expect(setSearchValueMock).toHaveBeenCalledTimes(1); - expect(setSearchValueMock).toHaveBeenCalledWith({ - key: 'page', - value: 1, - }); - setSearchValueMock.mockClear(); - - const searchBar = wrapper.getComponent('.sw-search-bar'); - await searchBar.vm.$emit('search', 'Nice theme'); - - expect(setSearchValueMock).toHaveBeenCalledWith({ - key: 'term', - value: 'Nice theme', - }); - }); - - it('should filter to only app extensions', async () => { - await createWrapper(); - - const filter = Shopware.Store.get('shopwareExtensions').search.filter; - - expect(filter).toEqual({ - group: 'apps', - }); - }); - - it('should filter to only theme extensions', async () => { - const wrapper = await createWrapper(); - - wrapper.vm.$route.name = 'sw.extension.store.listing.theme'; - await flushPromises(); - - const filter = Shopware.Store.get('shopwareExtensions').search.filter; - - expect(filter).toEqual({ - group: 'themes', - }); - }); - - it('should show update message when newer version is available', async () => { - // Mock higher `latestVersion` to show update card - myExtensionsMock.mockImplementationOnce(() => Promise.resolve([{ - name: 'SwagExtensionStore', - latestVersion: '1.4.0', - version: '1.3.0', - }])); - - const wrapper = await createWrapper(); - - // Wait for loader to disappear - await flushPromises(); - - expect(wrapper.get('sw-extension-store-update-warning-stub').exists()).toBe(true); - }); - - it('should show listing errors on listing errors event', async () => { - const wrapper = await createWrapper(); - - // Wait for loader to disappear - await flushPromises(); - - // Mock listing error response - const listingError = new Error(); - listingError.response = { - data: { - errors: [ - { - code: 'FRAMEWORK__STORE_ERROR', - detail: 'The given Shopware version is unknown, please contact our customer service', - meta: { - documentationLink: 'https://docs.shopware.com', - }, - status: '500', - title: 'Shopware version is unknown', - }, - ], - }, + it('should use the register service method', async () => { + const channelService = { + register: jest.fn(), }; + await createWrapper(channelService); - // Emit listing error on router view - await wrapper.getComponent('router-view-stub').vm.$emit('extension-listing-errors', listingError); - - expect(wrapper.get('sw-extension-store-error-card-stub').attributes().title) - .toBe('Shopware version is unknown'); - - expect(wrapper.get('sw-extension-store-error-card-stub').attributes().variant) - .toBe('danger'); - - expect(wrapper.get('sw-extension-store-error-card-stub').text()) - .toBe('The given Shopware version is unknown, please contact our customer service'); + expect(channelService.register).toBeCalled(); }); }); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/index.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/index.js deleted file mode 100644 index 82527a33..00000000 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/index.js +++ /dev/null @@ -1,106 +0,0 @@ -import template from './sw-extension-store-listing.html.twig'; -import './sw-extension-store-listing.scss'; - -/** - * @private - */ -export default { - name: 'sw-extension-store-listing', - template, - - inject: ['feature'], - - emits: ['extension-listing-errors'], - - mixins: ['sw-extension-error'], - - data() { - return { - isLoading: false, - }; - }, - - computed: { - extensions() { - return Shopware.Store.get('shopwareExtensions').extensionListing; - }, - - currentSearch() { - return Shopware.Store.get('shopwareExtensions').search; - }, - - page() { - return this.currentSearch.page; - }, - - limit() { - return this.currentSearch.limit; - }, - - total() { - return this.extensions.total || 0; - }, - - rating() { - return this.currentSearch.rating; - }, - - languageId() { - return Shopware.Store.get('session').languageId; - }, - - currentLocale() { - return Shopware.Store.get('session').currentLocale === 'de-DE' ? 'de' : 'en'; - }, - }, - - watch: { - currentSearch: { - deep: true, - immediate: true, - handler() { - this.getList(); - }, - }, - languageId(newValue) { - if (newValue !== '') { - this.getList(); - } - }, - }, - - methods: { - async getList() { - this.isLoading = true; - - if (this.languageId === '') { - return; - } - - try { - await this.search(); - } catch (e) { - this.showExtensionErrors(e); - this.$emit('extension-listing-errors', e); - } finally { - this.isLoading = false; - } - }, - - async search() { - const extensionDataService = Shopware.Service('extensionStoreDataService'); - - const page = await extensionDataService.getExtensionList( - Shopware.Store.get('shopwareExtensions').search, - { ...Shopware.Context.api, languageId: Shopware.Store.get('session').languageId }, - ); - - Shopware.Store.get('shopwareExtensions').extensionListing = page; - }, - - setPage({ limit, page }) { - Shopware.Store.get('shopwareExtensions').setSearchValue({ key: 'limit', value: limit }); - Shopware.Store.get('shopwareExtensions').setSearchValue({ key: 'page', value: page }); - }, - }, -}; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.html.twig deleted file mode 100644 index 27525907..00000000 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.html.twig +++ /dev/null @@ -1,28 +0,0 @@ -{% block sw_extension_store_listing %} -
- {% block sw_extension_store_listing_loader %} - - {% endblock %} - - - - - {% block sw_extension_store_listing_filter %} - - {% endblock %} - - {% block sw_extension_store_listing_grid %} -
- -
- {% endblock %} - - {% block sw_extension_store_listing_pagination %} - - {% endblock %} -
-{% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.scss b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.scss index 23662f82..e69de29b 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.scss +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-listing/sw-extension-store-listing.scss @@ -1,50 +0,0 @@ -@import "~scss/variables"; - -.sw-extension-store-listing { - margin: 0 auto; - max-width: 960px; - position: relative; - - .sw-extension-store-listing__banner { - margin-bottom: 40px; - cursor: pointer; - border-radius: 8px; - box-shadow: 0 0 4px 0 var(--color-elevation-shadow-default); - overflow: hidden; - display: flex; - - img { - width: 100%; - } - } - - .sw-extension-store-listing__listing-grid { - display: grid; - grid-gap: 24px; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - justify-content: space-between; - margin-bottom: 40px; - } - - .sw-pagination { - border: none; - background: none; - - .sw-pagination__per-page { - display: none; - } - } - - .sw-loader { - background: var(--color-elevation-surface-raised); - min-height: 300px; - - .sw-loader__container { - position: sticky; - } - } - - .sw-extension-store-statistics-promotion { - margin-bottom: 40px; - } -} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts new file mode 100644 index 00000000..bfc871f3 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -0,0 +1,156 @@ +import { handle } from '@shopware-ag/meteor-admin-sdk/es/channel'; +import type ExtensionStoreActionService from 'src/module/sw-extension/service/extension-store-action.service'; +import type ShopwareExtensionService from 'src/module/sw-extension/service/shopware-extension.service'; +import type ExtensionStoreLicensesService from './extension-store-licenses.service'; +import type { Router } from 'vue-router'; +import type { ShopwareMessageTypes } from '@shopware-ag/meteor-admin-sdk/es/message-types'; + +type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase'; + +type StoreChannelActionData = { + action: StoreChannelAction; +}; + +type HandshakeActionData = StoreChannelActionData & { + sessionToken: string; +}; + +type RouteToActionData = StoreChannelActionData & { + route: string; +}; + +type PurchaseActionData = StoreChannelActionData & { + productId: string; + variantId: string; + sessionToken: string; +}; + +type StoreContext = { + shopwareVersion: string; + owningExtensions: string[]; + sessionToken: string; + language: string; + isLoggedIn: boolean; + success: boolean; +}; + +type PurchaseResponse = { + sessionToken: string; + success: boolean; +}; + +export class ExtensionStoreChannelService { + constructor( + private readonly extensionStoreActionService: ExtensionStoreActionService, + private readonly shopwareExtensionService: ShopwareExtensionService, + private readonly extensionStoreLicensesService: ExtensionStoreLicensesService, + private readonly router: Router, + ) {} + + register(): void { + handle('swag-extension-store-channel' as keyof ShopwareMessageTypes, (data: unknown) => { + try { + return this.handleAction(data); + } catch (err) { + return { + success: false, + }; + } + }); + } + + private handleAction(data: unknown): Promise | void { + if (!this.isStoreChannelActionData(data)) { + return; + } + switch (data.action) { + case 'handshake': + if (!this.isHandshakeActionData(data)) { + return; + } + return this.handleHandshake(data); + case 'routeTo': + if (!this.isRouteToActionData(data)) { + return; + } + return this.handleRouteTo(data); + case 'purchase': + if (!this.isPurchaseActionData(data)) { + return; + } + return this.handlePurchase(data); + } + } + + private isStoreChannelActionData(data: unknown): data is StoreChannelActionData { + return ( + data !== null + && typeof data === 'object' + && 'action' in data + && typeof data.action === 'string' + ); + } + + private isHandshakeActionData(data: unknown): data is HandshakeActionData { + return ( + this.isStoreChannelActionData(data) + && 'sessionToken' in data + && typeof data.sessionToken === 'string' + ); + } + + private isRouteToActionData(data: unknown): data is RouteToActionData { + return ( + this.isStoreChannelActionData(data) + && 'route' in data + && typeof data.route === 'string' + ); + } + + private isPurchaseActionData(data: unknown): data is PurchaseActionData { + return ( + this.isStoreChannelActionData(data) + && 'productId' in data + && 'variantId' in data + && 'sessionToken' in data + && typeof data.productId === 'string' + && typeof data.variantId === 'string' + && typeof data.sessionToken === 'string' + ); + } + + private async handleHandshake(data: HandshakeActionData): Promise { + const extensions = (await this.extensionStoreActionService.getMyExtensions()).map((extension) => extension.name); + await this.shopwareExtensionService.checkLogin(); + const shopwareVersion = Shopware.Context.app.config.version ?? ''; + const language = Shopware.Context.app.fallbackLocale ?? ''; + const isLoggedIn = Shopware.Store.get('shopwareExtensions').userInfo !== null; + + return { + shopwareVersion: shopwareVersion, + owningExtensions: extensions, + sessionToken: data.sessionToken, + language: language, + isLoggedIn: isLoggedIn, + success: true, + }; + } + + private handleRouteTo(data: RouteToActionData): void { + this.router.push({ name: data.route }); + } + + private async handlePurchase(data: PurchaseActionData): Promise { + const cartResponse = await this.extensionStoreLicensesService.newCart( + data.productId, + data.variantId, + ); + + await this.extensionStoreLicensesService.orderCart(cartResponse.data); + + return { + sessionToken: data.sessionToken, + success: true, + }; + } +} diff --git a/src/Resources/app/administration/tsconfig.json b/src/Resources/app/administration/tsconfig.json index 89166841..7520cda4 100644 --- a/src/Resources/app/administration/tsconfig.json +++ b/src/Resources/app/administration/tsconfig.json @@ -26,18 +26,19 @@ "./src/*" ], "src/*": [ - "./../../../../../../../src/Administration/Resources/app/administration/src/*" + "./../../../../../../../src/Administration/Resources/app/administration/src/*", ], "*": [ "./node_modules/*", - "./../../../../../../../src/Administration/Resources/app/administration/node_modules/*" + "./../../../../../../../src/Administration/Resources/app/administration/node_modules/*", ] } }, "files": [ "./../../../../../../../src/Administration/Resources/app/administration/src/global.types.ts", "./../../../../../../../src/Administration/Resources/app/administration/src/entity-schema-definition.d.ts", - "./../../../../../../../src/Administration/Resources/app/administration/src/html-shim.d.ts" + "./../../../../../../../src/Administration/Resources/app/administration/src/html-shim.d.ts", + ], "include": [ "./src/**/*", From 84bd2172e780e905e9647b0e6f78894c54c39d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Wed, 29 Oct 2025 14:19:34 +0100 Subject: [PATCH 02/38] ECTO-2203 - add changes for checkout out of the evaluation to the feature branch --- src/Controller/LicenseController.php | 6 +++--- src/Services/LicenseService.php | 2 +- src/Services/StoreClient.php | 3 ++- tests/Controller/LicenseControllerTest.php | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Controller/LicenseController.php b/src/Controller/LicenseController.php index f75c9b99..529874e8 100644 --- a/src/Controller/LicenseController.php +++ b/src/Controller/LicenseController.php @@ -35,15 +35,15 @@ public function createCart(Request $request, Context $context): JsonResponse $extensionId = $request->request->get('extensionId'); $variantId = $request->request->get('variantId'); - if (!is_numeric($extensionId)) { + if (!\is_int($extensionId) && !\is_string($extensionId)) { throw ExtensionStoreException::invalidExtensionId(); } - if (!is_numeric($variantId)) { + if (!\is_int($variantId) && !\is_string($variantId)) { throw ExtensionStoreException::invalidVariantId(); } - $cart = $this->licenseService->createCart((int) $extensionId, (int) $variantId, $context); + $cart = $this->licenseService->createCart($extensionId, $variantId, $context); return new JsonResponse($cart); } diff --git a/src/Services/LicenseService.php b/src/Services/LicenseService.php index d30bd89a..af6563f2 100644 --- a/src/Services/LicenseService.php +++ b/src/Services/LicenseService.php @@ -22,7 +22,7 @@ public function __construct(StoreClient $client) $this->client = $client; } - public function createCart(int $extensionId, int $variantId, Context $context): CartStruct + public function createCart(int|string $extensionId, int|string $variantId, Context $context): CartStruct { $cart = $this->client->createCart($extensionId, $variantId, $context); diff --git a/src/Services/StoreClient.php b/src/Services/StoreClient.php index 94bd11fb..02d8cf0b 100644 --- a/src/Services/StoreClient.php +++ b/src/Services/StoreClient.php @@ -141,7 +141,7 @@ public function extensionDetailReviews(int $id, ExtensionCriteria $criteria, Con return json_decode((string) $response->getBody(), true); } - public function createCart(int $extensionId, int $variantId, Context $context): CartStruct + public function createCart(int|string $extensionId, int|string $variantId, Context $context): CartStruct { try { $response = $this->client->request( @@ -157,6 +157,7 @@ public function createCart(int $extensionId, int $variantId, Context $context): 'variantId' => $variantId, ], ], + 'mode' => 'newStore', ], ], ); diff --git a/tests/Controller/LicenseControllerTest.php b/tests/Controller/LicenseControllerTest.php index de356314..8a95c036 100644 --- a/tests/Controller/LicenseControllerTest.php +++ b/tests/Controller/LicenseControllerTest.php @@ -22,7 +22,7 @@ public function testPurchaseExtensionWithInvalidExtensionId(): void $controller = new LicenseController($this->createMock(LicenseService::class)); $request = new Request(); - $request->request->set('extensionId', 'foo'); + $request->request->set('extensionId', 2.4); $this->expectException(InvalidExtensionIdException::class); $controller->createCart($request, Context::createDefaultContext()); @@ -34,7 +34,7 @@ public function testPurchaseExtensionWithInvalidVariantId(): void $request = new Request(); $request->request->set('extensionId', 1); - $request->request->set('variantId', 'foo'); + $request->request->set('variantId', 2.4); $this->expectException(InvalidVariantIdException::class); $controller->createCart($request, Context::createDefaultContext()); From e1e725b387ee890c2719c032954026c1de1e2974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Mon, 1 Dec 2025 16:03:51 +0100 Subject: [PATCH 03/38] ECTO-2282 - make iframe url configurable (#126) --- composer.json | 2 +- .../page/sw-extension-store-index/index.js | 19 ++++++++++++++--- .../sw-extension-store-index.html.twig | 6 +++++- .../sw-extension-store-index.spec.js | 21 ++++++++++++++++++- src/Resources/config/config.xml | 11 ++++++++++ 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/Resources/config/config.xml diff --git a/composer.json b/composer.json index a48eca75..498f2128 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "swag/swag-extension-store", - "version": "4.2.0", + "version": "5.0.0", "description": "SWAG Extension Store", "type": "shopware-platform-plugin", "license": "MIT", diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js index a23c859a..71b355f7 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js @@ -8,15 +8,28 @@ export default { template, // we have to add the extensionStoreLicensesService for the checkout example - inject: ['extensionStoreChannelService'], + inject: [ + 'extensionStoreChannelService', + 'systemConfigApiService', + ], data() { return { - + storeUrl: null, }; }, - created() { + async created() { this.extensionStoreChannelService.register(); + + try { + // TODO: find other way to store the iframe URL + const config = await this.systemConfigApiService.getValues('SwagExtensionStore.config'); + + this.storeUrl = config['SwagExtensionStore.config.iframeUrl']; + + } catch (e) { + // Fallback to default store URL if config fetch fails + } }, }; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig index d9b07dbf..a657478c 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig @@ -1,3 +1,7 @@ {% block sw_extension_store_index %} - + {% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js index 251ad1ee..947bf1d8 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js @@ -6,12 +6,20 @@ Shopware.Component.register( () => import('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-index'), ); -async function createWrapper(channelService = null) { +async function createWrapper(channelService = null, systemConfigService = null) { if (channelService === null) { channelService = { register: jest.fn(), }; } + if (systemConfigService === null) { + systemConfigService = { + getValues: jest.fn().mockResolvedValue({ + 'SwagExtensionStore.config.iframeUrl': 'https://store.shopware.com', + }), + }; + } + return mount(await Shopware.Component.build('sw-extension-store-index'), { props: {}, global: { @@ -29,6 +37,7 @@ async function createWrapper(channelService = null) { }, provide: { extensionStoreChannelService: channelService, + systemConfigApiService: systemConfigService, }, }, }); @@ -49,4 +58,14 @@ describe('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-i expect(channelService.register).toBeCalled(); }); + + it ('should fetch the store URL from system config', async () => { + const systemConfigService = { + getValues: jest.fn(), + }; + + await createWrapper(null, systemConfigService); + + expect(systemConfigService.getValues).toBeCalledWith('SwagExtensionStore.config'); + }); }); diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml new file mode 100644 index 00000000..61cc0b63 --- /dev/null +++ b/src/Resources/config/config.xml @@ -0,0 +1,11 @@ + + + + Extension Store + + iframeUrl + + + + From 13ee3ca407cf3bb0910be0f66f19fea33711be37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Thu, 4 Dec 2025 10:47:21 +0100 Subject: [PATCH 04/38] ECTO-2328 - changed language handover (#128) --- .../service/extension-store-channel.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts index bfc871f3..0955af1c 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -123,7 +123,8 @@ export class ExtensionStoreChannelService { const extensions = (await this.extensionStoreActionService.getMyExtensions()).map((extension) => extension.name); await this.shopwareExtensionService.checkLogin(); const shopwareVersion = Shopware.Context.app.config.version ?? ''; - const language = Shopware.Context.app.fallbackLocale ?? ''; + const rawLocale: unknown = Shopware.Store.get('session')?.currentLocale; + const language: string = typeof rawLocale === 'string' ? rawLocale : 'en-GB'; const isLoggedIn = Shopware.Store.get('shopwareExtensions').userInfo !== null; return { From 34483e52db9989858a321d6566b320241c0a0b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Mon, 5 Jan 2026 16:21:23 +0100 Subject: [PATCH 05/38] ECTO-2339 - added approach for deep lining into the iframe store (#129) --- .../app/administration/jest.config.js | 1 + .../src/module/sw-extension-store/index.js | 3 +- .../page/sw-extension-store-index/index.js | 4 ++ .../sw-extension-store-index.spec.js | 3 + .../extension-store-channel.service.ts | 60 ++++++++++++++++++- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/Resources/app/administration/jest.config.js b/src/Resources/app/administration/jest.config.js index f34ff8eb..bb2a01cc 100644 --- a/src/Resources/app/administration/jest.config.js +++ b/src/Resources/app/administration/jest.config.js @@ -45,6 +45,7 @@ module.exports = { '^@shopware-ag/meteor-component-library$': `${process.env.ADMIN_PATH}/node_modules/@shopware-ag/meteor-component-library/dist/common/index.js`, vue$: `${process.env.ADMIN_PATH}/node_modules/vue/dist/vue.cjs.js`, '^@vue/test-utils$': `${process.env.ADMIN_PATH}/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js`, + '^lodash-es/(.*)$': 'lodash/$1', }, testEnvironmentOptions: { diff --git a/src/Resources/app/administration/src/module/sw-extension-store/index.js b/src/Resources/app/administration/src/module/sw-extension-store/index.js index e3ae5658..17845e76 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/index.js @@ -58,7 +58,8 @@ Shopware.Module.register('sw-extension-store', { routePrefixPath: 'sw/extension', routes: { store: { - path: 'store*', + path: 'store/:pathMatch(.*)*', + name: 'Store', meta: { privilege: 'system.extension_store', }, diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js index 71b355f7..16c83569 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/index.js @@ -32,4 +32,8 @@ export default { // Fallback to default store URL if config fetch fails } }, + + beforeUnmount() { + this.extensionStoreChannelService.unregister(); + }, }; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js index 947bf1d8..a6ae133d 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js @@ -10,6 +10,7 @@ async function createWrapper(channelService = null, systemConfigService = null) if (channelService === null) { channelService = { register: jest.fn(), + unregister: jest.fn(), }; } if (systemConfigService === null) { @@ -53,6 +54,7 @@ describe('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-i it('should use the register service method', async () => { const channelService = { register: jest.fn(), + unregister: jest.fn(), }; await createWrapper(channelService); @@ -62,6 +64,7 @@ describe('SwagExtensionStore/module/sw-extension-store/page/sw-extension-store-i it ('should fetch the store URL from system config', async () => { const systemConfigService = { getValues: jest.fn(), + unregister: jest.fn(), }; await createWrapper(null, systemConfigService); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts index 0955af1c..7a897a2b 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -5,7 +5,7 @@ import type ExtensionStoreLicensesService from './extension-store-licenses.servi import type { Router } from 'vue-router'; import type { ShopwareMessageTypes } from '@shopware-ag/meteor-admin-sdk/es/message-types'; -type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase'; +type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase' | 'routerUpdate'; type StoreChannelActionData = { action: StoreChannelAction; @@ -19,6 +19,10 @@ type RouteToActionData = StoreChannelActionData & { route: string; }; +type RouterUpdateActionData = StoreChannelActionData & { + urlSegments: string[]; +}; + type PurchaseActionData = StoreChannelActionData & { productId: string; variantId: string; @@ -32,6 +36,7 @@ type StoreContext = { language: string; isLoggedIn: boolean; success: boolean; + currentRoute: string; }; type PurchaseResponse = { @@ -40,6 +45,8 @@ type PurchaseResponse = { }; export class ExtensionStoreChannelService { + private unsubscribeFunction?: () => void; + constructor( private readonly extensionStoreActionService: ExtensionStoreActionService, private readonly shopwareExtensionService: ShopwareExtensionService, @@ -48,7 +55,11 @@ export class ExtensionStoreChannelService { ) {} register(): void { - handle('swag-extension-store-channel' as keyof ShopwareMessageTypes, (data: unknown) => { + if (this.unsubscribeFunction) { + return; + } + + this.unsubscribeFunction = handle('swag-extension-store-channel' as keyof ShopwareMessageTypes, (data: unknown) => { try { return this.handleAction(data); } catch (err) { @@ -59,6 +70,15 @@ export class ExtensionStoreChannelService { }); } + unregister(): void { + if (!this.unsubscribeFunction) { + return; + } + + this.unsubscribeFunction(); + this.unsubscribeFunction = undefined; + } + private handleAction(data: unknown): Promise | void { if (!this.isStoreChannelActionData(data)) { return; @@ -79,6 +99,11 @@ export class ExtensionStoreChannelService { return; } return this.handlePurchase(data); + case 'routerUpdate': + if (!this.isRouterUpdateActionData(data)) { + return; + } + return this.handleRouterUpdate(data); } } @@ -119,6 +144,15 @@ export class ExtensionStoreChannelService { ); } + private isRouterUpdateActionData(data: unknown): data is RouterUpdateActionData { + return ( + this.isStoreChannelActionData(data) + && 'urlSegments' in data + && Array.isArray((data as { urlSegments: unknown }).urlSegments) + && (data as { urlSegments: unknown[] }).urlSegments.every((segment: unknown) => typeof segment === 'string') + ); + } + private async handleHandshake(data: HandshakeActionData): Promise { const extensions = (await this.extensionStoreActionService.getMyExtensions()).map((extension) => extension.name); await this.shopwareExtensionService.checkLogin(); @@ -126,6 +160,7 @@ export class ExtensionStoreChannelService { const rawLocale: unknown = Shopware.Store.get('session')?.currentLocale; const language: string = typeof rawLocale === 'string' ? rawLocale : 'en-GB'; const isLoggedIn = Shopware.Store.get('shopwareExtensions').userInfo !== null; + const currentRoute = Object.values(this.router.currentRoute.value.params.pathMatch || {}).join('/'); return { shopwareVersion: shopwareVersion, @@ -134,6 +169,7 @@ export class ExtensionStoreChannelService { language: language, isLoggedIn: isLoggedIn, success: true, + currentRoute: currentRoute, }; } @@ -154,4 +190,24 @@ export class ExtensionStoreChannelService { success: true, }; } + + private handleRouterUpdate(data: RouterUpdateActionData): void { + const current = this.router.currentRoute.value; + let newPath = current.path; + if (current.params?.pathMatch?.length > 0) { + newPath = newPath.split('/').filter(segment => !current.params.pathMatch.includes(segment)).join('/'); + } + if (data.urlSegments.length > 0) { + const segmentsToAdd = data.urlSegments.join('/'); + newPath += `/${segmentsToAdd}`; + } + + if (newPath !== current.path) { + this.router.replace({ + path: newPath, + query: current.query, + hash: current.hash, + }); + } + } } From 0a6ed1790a56c064983703db60181422f4622728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Fri, 13 Feb 2026 08:10:04 +0100 Subject: [PATCH 06/38] ECTO-2406 - added post message function for copy to clipboard (#130) --- .../extension-store-channel.service.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts index 7a897a2b..e384c2a1 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -5,7 +5,7 @@ import type ExtensionStoreLicensesService from './extension-store-licenses.servi import type { Router } from 'vue-router'; import type { ShopwareMessageTypes } from '@shopware-ag/meteor-admin-sdk/es/message-types'; -type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase' | 'routerUpdate'; +type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase' | 'routerUpdate' | 'copyToClipboard'; type StoreChannelActionData = { action: StoreChannelAction; @@ -44,6 +44,10 @@ type PurchaseResponse = { success: boolean; }; +type CopyToClipboardActionData = StoreChannelActionData & { + text: string; +}; + export class ExtensionStoreChannelService { private unsubscribeFunction?: () => void; @@ -104,6 +108,11 @@ export class ExtensionStoreChannelService { return; } return this.handleRouterUpdate(data); + case 'copyToClipboard': + if ((!this.isCopyToClipboardActionData(data))) { + return; + } + return this.handleCopyToClipboard(data); } } @@ -132,6 +141,14 @@ export class ExtensionStoreChannelService { ); } + private isCopyToClipboardActionData(data: unknown): data is CopyToClipboardActionData { + return ( + this.isStoreChannelActionData(data) + && 'text' in data + && typeof data.text === 'string' + ); + } + private isPurchaseActionData(data: unknown): data is PurchaseActionData { return ( this.isStoreChannelActionData(data) @@ -210,4 +227,10 @@ export class ExtensionStoreChannelService { }); } } + + private handleCopyToClipboard(data: StoreChannelActionData & { text: string }): void { + navigator.clipboard.writeText(data.text).catch((err) => { + console.error('Failed to copy text to clipboard', err); + }); + } } From 016c305a0ad9a7fa450303d19e1ff31a956f11e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20B=C3=BCscher?= Date: Mon, 23 Feb 2026 16:04:52 +0100 Subject: [PATCH 07/38] ECTO-2395 - implemented rough woring checkout with sample modal (#131) --- .../index.ts | 40 +++++++++ ...tore-purchase-confirmation-modal.html.twig | 45 ++++++++++ ...ion-store-purchase-confirmation-modal.scss | 26 ++++++ .../src/module/sw-extension-store/index.js | 3 + .../sw-extension-store-index.html.twig | 2 + .../sw-extension-store-index.spec.js | 1 + .../extension-store-channel.service.ts | 89 ++++++++++++++++++- .../module/sw-extension-store/snippet/de.json | 12 +++ .../module/sw-extension-store/snippet/en.json | 12 +++ ...nsion-store-purchase-confirmation.store.ts | 67 ++++++++++++++ .../types/extension-store-basket.types.ts | 61 +++++++++++++ src/Services/StoreClient.php | 2 +- 12 files changed, 355 insertions(+), 5 deletions(-) create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts new file mode 100644 index 00000000..c85eb80f --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts @@ -0,0 +1,40 @@ +import { purchaseConfirmationStore } from '../../store/extension-store-purchase-confirmation.store'; +import template from './sw-extension-store-purchase-confirmation-modal.html.twig'; +import './sw-extension-store-purchase-confirmation-modal.scss'; + +/** + * @private + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + data() { + return { + store: purchaseConfirmationStore, + }; + }, + + computed: { + isOpen() { + return this.store.state.isOpen; + }, + + isLoading() { + return this.store.state.isLoading; + }, + + cartData() { + return this.store.state.cartData; + }, + }, + + methods: { + async onConfirmPurchase() { + await this.store.confirm(); + }, + + onCancelPurchase() { + this.store.cancel(); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig new file mode 100644 index 00000000..baddf71c --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig @@ -0,0 +1,45 @@ +{% block sw_extension_store_purchase_confirmation_modal %} + + {% block sw_extension_store_purchase_confirmation_modal_content %} +
+ {% block sw_extension_store_purchase_confirmation_modal_cart_data %} +
+

{{ $t('sw-extension-store.purchase-confirmation.description') }}

+ + +
+
{{ JSON.stringify(cartData, null, 2) }}
+
+
+ {% endblock %} +
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_modal_footer %} + + {% endblock %} +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss new file mode 100644 index 00000000..301fb62d --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss @@ -0,0 +1,26 @@ +.sw-extension-store-purchase-confirmation-modal { + &__content { + padding: 20px 0; + } + + &__cart-data { + margin-bottom: 20px; + } + + &__cart-info { + background-color: #f5f5f5; + border: 1px solid #d1d5db; + border-radius: 4px; + padding: 16px; + margin-top: 12px; + max-height: 400px; + overflow-y: auto; + + pre { + margin: 0; + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; + } + } +} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/index.js b/src/Resources/app/administration/src/module/sw-extension-store/index.js index 17845e76..6643a8b7 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/index.js @@ -10,6 +10,7 @@ Shopware.Component.register('sw-extension-store-index', () => import('./page/sw- Shopware.Component.register('sw-extension-store-slider', () => import('./component/sw-extension-store-slider')); Shopware.Component.register('sw-extension-store-listing-filter', () => import('./component/sw-extension-store-listing-filter')); Shopware.Component.register('sw-extension-buy-modal', () => import('./component/sw-extension-buy-modal')); +Shopware.Component.register('sw-extension-store-purchase-confirmation-modal', () => import('./component/sw-extension-store-purchase-confirmation-modal')); Shopware.Component.register('sw-extension-listing-card', () => import('./component/sw-extension-listing-card')); Shopware.Component.register('sw-extension-store-update-warning', () => import('./component/sw-extension-store-update-warning')); Shopware.Component.register('sw-extension-label', () => import('./component/sw-extension-store-label')); @@ -33,6 +34,8 @@ Shopware.Application.addServiceProvider('extensionStoreChannelService', () => { Shopware.Service('extensionStoreActionService'), Shopware.Service('shopwareExtensionService'), Shopware.Service('extensionStoreLicensesService'), + Shopware.Service('extensionHelperService'), + Shopware.Service('cacheApiService'), Shopware.Application.view.router, ); }); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig index a657478c..1d119092 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig @@ -4,4 +4,6 @@ :src="storeUrl" location-id="" /> + + {% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js index a6ae133d..c86ffbf1 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.spec.js @@ -35,6 +35,7 @@ async function createWrapper(channelService = null, systemConfigService = null) }, stubs: { 'sw-iframe-renderer': true, + 'sw-extension-store-purchase-confirmation-modal': true, }, provide: { extensionStoreChannelService: channelService, diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts index e384c2a1..83bf57f2 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -1,9 +1,13 @@ -import { handle } from '@shopware-ag/meteor-admin-sdk/es/channel'; +import { handle, publish } from '@shopware-ag/meteor-admin-sdk/es/channel'; import type ExtensionStoreActionService from 'src/module/sw-extension/service/extension-store-action.service'; import type ShopwareExtensionService from 'src/module/sw-extension/service/shopware-extension.service'; import type ExtensionStoreLicensesService from './extension-store-licenses.service'; import type { Router } from 'vue-router'; import type { ShopwareMessageTypes } from '@shopware-ag/meteor-admin-sdk/es/message-types'; +import { purchaseConfirmationStore } from '../store/extension-store-purchase-confirmation.store'; +import type ExtensionHelperService from 'src/app/service/extension-helper.service'; +import type CacheApiService from 'src/core/service/api/cache.api.service'; +import type { ExtensionStoreBasket } from '../types/extension-store-basket.types'; type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase' | 'routerUpdate' | 'copyToClipboard'; @@ -42,6 +46,13 @@ type StoreContext = { type PurchaseResponse = { sessionToken: string; success: boolean; + +}; + +type PurchaseResultData = { + action: 'purchaseResult'; + sessionToken: string; + success: boolean; }; type CopyToClipboardActionData = StoreChannelActionData & { @@ -55,6 +66,8 @@ export class ExtensionStoreChannelService { private readonly extensionStoreActionService: ExtensionStoreActionService, private readonly shopwareExtensionService: ShopwareExtensionService, private readonly extensionStoreLicensesService: ExtensionStoreLicensesService, + private readonly extensionHelperService: ExtensionHelperService, + private readonly cacheApiService: CacheApiService, private readonly router: Router, ) {} @@ -195,19 +208,45 @@ export class ExtensionStoreChannelService { } private async handlePurchase(data: PurchaseActionData): Promise { - const cartResponse = await this.extensionStoreLicensesService.newCart( + const cartData = (await this.extensionStoreLicensesService.newCart( data.productId, data.variantId, - ); + )).data as ExtensionStoreBasket; - await this.extensionStoreLicensesService.orderCart(cartResponse.data); + purchaseConfirmationStore.openModal( + cartData, + async () => { + await this.performPurchase(data, cartData); + }, + () => { + this.publishPurchaseResult(data.sessionToken, false); + }, + ); + // Respond immediately to the iframe so the channel does not time out. + // The final purchase result is communicated via a separate published message. return { sessionToken: data.sessionToken, success: true, }; } + private publishPurchaseResult( + sessionToken: string, + success: boolean, + ): void { + const resultData: PurchaseResultData = { + action: 'purchaseResult', + sessionToken, + success, + }; + + publish( + 'swag-extension-store-channel' as keyof ShopwareMessageTypes, + resultData, + ); + } + private handleRouterUpdate(data: RouterUpdateActionData): void { const current = this.router.currentRoute.value; let newPath = current.path; @@ -233,4 +272,46 @@ export class ExtensionStoreChannelService { console.error('Failed to copy text to clipboard', err); }); } + + private async performPurchase(data: PurchaseActionData, cartData: ExtensionStoreBasket): Promise { + try { + await this.extensionStoreLicensesService.orderCart(cartData); + this.publishPurchaseResult(data.sessionToken, true); + } catch { + this.publishPurchaseResult(data.sessionToken, false); + return; + } + + await this.shopwareExtensionService.updateExtensionData(); + await this.installExtension(cartData); + } + + private async installExtension(cartData: ExtensionStoreBasket): Promise { + const extension = cartData.positions[0].extension; + const snippetService = Shopware.Snippet as unknown as { tc: (key: string, params?: Record) => string }; + + try { + await this.extensionHelperService.downloadAndActivateExtension(extension.name, extension.type); + + if (extension.type === 'plugin') { + await this.cacheApiService.clear(); + } + + Shopware.Store.get('notification').createNotification({ + variant: 'positive', + title: snippetService.tc('sw-extension-store.installation.successTitle'), + message: snippetService.tc('sw-extension-store.installation.successMessage', { name: String(extension.name) }), + growl: true, + }); + } catch (error) { + console.error('Failed to install extension after purchase', error); + + Shopware.Store.get('notification').createNotification({ + variant: 'critical', + title: snippetService.tc('sw-extension-store.installation.errorTitle'), + message: snippetService.tc('sw-extension-store.installation.errorMessage', { name: String(extension.name) }), + growl: true, + }); + } + } } diff --git a/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json b/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json index effdbae2..7e030e80 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json +++ b/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json @@ -86,6 +86,18 @@ }, "in-app-purchases": { "onceOff": "einmalig" + }, + "purchase-confirmation": { + "modalTitle": "Kauf bestätigen", + "description": "Bitte überprüfe die Kaufdetails vor der Bestätigung:", + "buyButton": "Kaufen", + "cancelButton": "Abbrechen" + }, + "installation": { + "successTitle": "Installation erfolgreich", + "successMessage": "{name} wurde erfolgreich installiert und aktiviert.", + "errorTitle": "Installation fehlgeschlagen", + "errorMessage": "{name} konnte nicht installiert werden. Bitte versuche es erneut oder installiere es manuell." } }, "sw-extension-store-statistics-promotion": { diff --git a/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json b/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json index 7b0cd762..2f6d6844 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json +++ b/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json @@ -86,6 +86,18 @@ }, "in-app-purchases": { "onceOff": "one-time" + }, + "purchase-confirmation": { + "modalTitle": "Confirm Purchase", + "description": "Please review your purchase details before confirming:", + "buyButton": "Buy", + "cancelButton": "Cancel" + }, + "installation": { + "successTitle": "Installation successful", + "successMessage": "{name} has been successfully installed and activated.", + "errorTitle": "Installation failed", + "errorMessage": "{name} could not be installed. Please try again or install it manually." } }, "sw-extension-store-statistics-promotion": { diff --git a/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts b/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts new file mode 100644 index 00000000..8414a018 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts @@ -0,0 +1,67 @@ +import { reactive } from 'vue'; +import type { ExtensionStoreBasket } from '../types/extension-store-basket.types'; + +type OnConfirmCallback = () => Promise; +type OnCancelCallback = () => void; + +type PurchaseConfirmationState = { + isOpen: boolean; + isLoading: boolean; + cartData: ExtensionStoreBasket | null; + onConfirm: OnConfirmCallback | null; + onCancel: OnCancelCallback | null; +}; + +const state = reactive({ + isOpen: false, + isLoading: false, + cartData: null, + onConfirm: null, + onCancel: null, +}); + +export const purchaseConfirmationStore = { + get state() { + return state; + }, + + openModal( + cartResponse: ExtensionStoreBasket, + onConfirm: OnConfirmCallback, + onCancel: OnCancelCallback, + ): void { + state.cartData = cartResponse; + state.onConfirm = onConfirm; + state.onCancel = onCancel; + state.isOpen = true; + }, + + async confirm(): Promise { + if (!state.onConfirm) { + return; + } + + state.isLoading = true; + + try { + await state.onConfirm(); + } finally { + this.closeModal(); + } + }, + + cancel(): void { + if (state.onCancel) { + state.onCancel(); + } + this.closeModal(); + }, + + closeModal(): void { + state.isOpen = false; + state.isLoading = false; + state.cartData = null; + state.onConfirm = null; + state.onCancel = null; + }, +}; diff --git a/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts b/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts new file mode 100644 index 00000000..8a669ff9 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts @@ -0,0 +1,61 @@ +type ExtensionStoreBasketShop = { + id: number; + domain: string; +}; + +type ExtensionStoreBasketExtension = { + id: number; + name: string; + type: 'app' | 'plugin'; +}; + +type ExtensionStoreBasketVariant = { + id: number; + name: string; + label: string | null; + duration: number | null; +}; + +type ExtensionStoreBasketPosition = { + extension: ExtensionStoreBasketExtension; + variant: ExtensionStoreBasketVariant; + taxRate: number | null; + netPrice: number; + taxValue: number; + grossPrice: number; + firstMonthFree: boolean; + pseudoPrice: number | null; + discountAppliesForMonths: number | null; + legalText: string | null; +}; + +type ExtensionStoreBasketPayment = { + chargingAmount: number; + paymentMean: { id: number } | null; + paymentText: string | null; + paymentMeanRequired: boolean; + registrationUrl: string; +}; + +type ExtensionStoreBasket = { + bookingShop: ExtensionStoreBasketShop & { balance: number }; + licenseShop: ExtensionStoreBasketShop; + /** @deprecated use licenseShop - kept for backwards compatibility */ + shop: ExtensionStoreBasketShop; + positions: ExtensionStoreBasketPosition[]; + netPrice: number; + taxRate: number; + taxValue: number; + grossPrice: number; + payment: ExtensionStoreBasketPayment; + legalText: string | null; +}; + +export type { + ExtensionStoreBasketShop, + ExtensionStoreBasketExtension, + ExtensionStoreBasketVariant, + ExtensionStoreBasketPosition, + ExtensionStoreBasketPayment, + ExtensionStoreBasket, +}; diff --git a/src/Services/StoreClient.php b/src/Services/StoreClient.php index 02d8cf0b..8f10bf83 100644 --- a/src/Services/StoreClient.php +++ b/src/Services/StoreClient.php @@ -157,7 +157,7 @@ public function createCart(int|string $extensionId, int|string $variantId, Conte 'variantId' => $variantId, ], ], - 'mode' => 'newStore', + 'identifierBase' => 'storeUuid', ], ], ); From 776f99eacf344b66fd1881d2e4383a15fbfe199c Mon Sep 17 00:00:00 2001 From: Kai Gossel Date: Mon, 16 Mar 2026 15:21:41 +0100 Subject: [PATCH 08/38] ECTO-2396 - add checkout process implementation (#132) --- .../index.ts | 87 +++++++ ...e-confirmation-checkout-overview.html.twig | 188 ++++++++++++++ ...rchase-confirmation-checkout-overview.scss | 144 +++++++++++ .../index.ts | 230 ++++++++++++++++++ ...e-purchase-confirmation-checkout.html.twig | 186 ++++++++++++++ ...-store-purchase-confirmation-checkout.scss | 112 +++++++++ .../index.ts | 31 ++- ...tore-purchase-confirmation-modal.html.twig | 67 ++--- ...ion-store-purchase-confirmation-modal.scss | 199 ++++++++++++++- .../index.ts | 109 +++++++++ ...urchase-confirmation-permissions.html.twig | 159 ++++++++++++ ...ore-purchase-confirmation-permissions.scss | 189 ++++++++++++++ .../src/module/sw-extension-store/index.js | 3 + .../sw-extension-store-index.html.twig | 6 +- .../extension-store-channel.service.ts | 22 +- .../module/sw-extension-store/snippet/de.json | 75 +++++- .../module/sw-extension-store/snippet/en.json | 77 +++++- ...nsion-store-purchase-confirmation.store.ts | 29 ++- .../types/extension-store-basket.types.ts | 35 +++ src/Services/LicenseService.php | 19 +- 20 files changed, 1888 insertions(+), 79 deletions(-) create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/index.ts create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.scss create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/index.ts create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.html.twig create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.scss create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/index.ts create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.html.twig create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.scss diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/index.ts b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/index.ts new file mode 100644 index 00000000..1d1031b6 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/index.ts @@ -0,0 +1,87 @@ +import template from './sw-extension-store-purchase-confirmation-checkout-overview.html.twig'; +import './sw-extension-store-purchase-confirmation-checkout-overview.scss'; + +import type { ExtensionStoreBasket, ExtensionStorePaymentMean } from '../../types/extension-store-basket.types'; + +/** + * @private + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + props: { + cart: { + type: Object as PropType, + required: true, + }, + defaultPaymentMean: { + type: Object as PropType, + required: true, + }, + editable: { + type: Boolean, + required: false, + default: true, + }, + hasPaymentMethodError: { + type: Boolean, + required: false, + default: false, + }, + }, + + data() { + return { + showPaymentMethodBanner: this.hasPaymentMethodError, + }; + }, + + computed: { + position() { + return this.cart?.positions?.[0]; + }, + + extension() { + return this.position?.extension; + }, + + producer() { + return this.extension?.producer; + }, + + bookingShop() { + return this.cart?.bookingShop; + }, + + bookingShopDomain() { + const domain = this.bookingShop?.domain ?? ''; + + if (!domain) { + return ''; + } + + // If the domain does not start with http:// or https://, prepend https:// + if (!/^https?:\/\//i.test(domain)) { + return `https://${domain}`; + } + + return domain; + }, + + billingAddress() { + return this.cart?.billingAddress; + }, + + cartHasBillingAddress() { + return this.billingAddress?.fullName + && this.billingAddress.street + && this.billingAddress.city + && this.billingAddress.country + && this.billingAddress.zip; + }, + + payment() { + return this.cart?.payment; + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig new file mode 100644 index 00000000..5a1078c0 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig @@ -0,0 +1,188 @@ + {% block sw_extension_store_purchase_confirmation_checkout_overview %} +
+ {% block sw_extension_store_purchase_confirmation_checkout_overview_details %} +
+ +
+
+ {{ extension.label }} +
+
+ {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.by') }} + + + {{ producer.name }} + + + + {{ producer.name }} + +
+
+
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_overview_info %} +
+ {% block sw_extension_store_purchase_confirmation_checkout_overview_info_booking_domain %} +
+
+ {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.bookingDomain') }}: +
+
+ + {{ bookingShop.domain }} + +
+
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_overview_info_billing_address %} +
+
+ {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.billingAddress') }}: +
+
+
+ {{ billingAddress.companyName }} +
+
+
+ {{ billingAddress.fullName }} +
+
+ {{ billingAddress.street.replace(',', '') }}, {{ billingAddress.zip }} {{ billingAddress.city }} +
+
+ {{ billingAddress.country }} +
+
+
+
+ + {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.change') }} + +
+
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_overview_info_payment_method %} +
+
+ {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.paymentMethod') }}: +
+
+ + + +
+
+ + {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.change') }} + + + {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.addPaymentMethod') }} + +
+
+ {% endblock %} +
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_overview_payment_method_banner %} + + + {% endblock %} +
+ {% endblock %} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.scss b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.scss new file mode 100644 index 00000000..714c77bf --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.scss @@ -0,0 +1,144 @@ +.sw-extension-store-purchase-confirmation__checkout-overview { + border: 1px solid var(--color-border-secondary-default); + border-radius: var(--border-radius-card); + display: flex; + flex-direction: column; + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + overflow: hidden; + + &-details { + align-items: center; + background: var(--color-background-secondary-default); + display: flex; + gap: 12px; + padding: 24px; + + &-icon { + border-radius: var(--border-radius-card); + height: 56px; + overflow: hidden; + width: 56px; + + &::after { + display: none; + } + } + + &-info { + &-name { + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-s); + } + + &-producer-name { + color: var(--color-text-secondary-default); + + .mt-link { + color: var(--color-text-primary-default); + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + } + } + + &-info { + display: flex; + flex-direction: column; + padding: 8px 24px; + + &-row { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + flex-direction: column; + padding: 12px 0; + + &:first-child { + border-top: none; + } + + @media screen and (min-width: 768px) { + flex-direction: row; + } + + &-label { + color: var(--color-text-secondary-default); + + @media screen and (min-width: 768px) { + width: 180px; + } + } + + &-value { + color: var(--color-text-primary-default); + font-weight: var(--font-weight-medium); + gap: 4px; + + &.is--payment-method { + align-items: center; + display: flex; + } + + &-address { + color: var(--color-text-secondary-default); + display: flex; + flex-direction: column; + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + } + + &-payment-method-label { + color: var(--color-text-secondary-default); + font-weight: var(--font-weight-regular); + } + + .sw-label { + margin: 0; + } + + .mt-link { + color: var(--color-text-primary-default); + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + &-action { + @media screen and (min-width: 768px) { + margin-left: auto; + } + + .mt-link { + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + } + } + + &-payment-method-banner { + padding: var(--scale-size-8) var(--scale-size-24) var(--scale-size-20) var(--scale-size-24); + + .mt-banner { + margin: 0; + + .mt-button { + margin-top: var(--scale-size-12); + } + } + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/index.ts b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/index.ts new file mode 100644 index 00000000..b74a8412 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/index.ts @@ -0,0 +1,230 @@ +import { purchaseConfirmationStore } from '../../store/extension-store-purchase-confirmation.store'; +import template from './sw-extension-store-purchase-confirmation-checkout.html.twig'; +import './sw-extension-store-purchase-confirmation-checkout.scss'; + +import type { ExtensionStoreBasket, ExtensionStorePaymentMean } from '../../types/extension-store-basket.types'; + +const { Utils } = Shopware; + +/** + * @private + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + props: { + cart: { + type: Object as PropType, + required: true, + }, + paymentMeans: { + type: Object as PropType, + required: true, + }, + tocAccepted: { + type: Boolean, + required: true, + }, + permissionsAccepted: { + type: Boolean, + required: true, + }, + }, + + emits: [ + 'update:modal-view', + 'update:toc-accepted', + 'update:permissions-accepted', + ], + + data() { + return { + store: purchaseConfirmationStore, + }; + }, + + computed: { + steps() { + return Object.freeze({ + CHECKOUT: null, + SUCCESS: 'success', + FAILED: 'failed', + }); + }, + + step() { + if (this.isSubmitted && !this.isLoading) { + return this.store.state.isSuccessful ? this.steps.SUCCESS : this.steps.FAILED; + } + + return this.steps.CHECKOUT; + }, + + isOpen() { + return this.store.state.isOpen; + }, + + isLoading() { + return this.store.state.isLoading; + }, + + isSubmitted() { + return this.store.state.isSubmitted; + }, + + position() { + return this.cart?.positions?.[0]; + }, + + extension() { + return this.position?.extension; + }, + + permissions() { + return this.extension?.permissions; + }, + + domains() { + return this.extension?.domains.filter((domain) => domain !== null); + }, + + defaultPaymentMean() { + const cartPaymentMeanId = this.cart?.payment?.paymentMean?.id; + + return this.paymentMeans?.find((paymentMean) => paymentMean.id === cartPaymentMeanId); + }, + + extensionHasPermissions() { + return !!Object.keys(this.permissions ?? {}).length; + }, + + extensionHasDomains() { + return (this.domains?.length ?? 0) > 0; + }, + + extensionHasPermissionsOrDomains() { + return this.extensionHasPermissions || this.extensionHasDomains; + }, + + isDiscounted() { + return this.position && this.position.netPrice !== this.position.pseudoPrice; + }, + + isFirstMonthFree() { + return this.position && this.position.firstMonthFree; + }, + + durationIntervalSnippet() { + const duration = this.position?.variant.duration; + + switch (duration) { + case 12: + return this.$t('sw-extension-store.purchase-confirmation.checkout.order.perYear'); + case 1: + return this.$t('sw-extension-store.purchase-confirmation.checkout.order.perMonth'); + default: + return null; + } + }, + + netPrice() { + let price = 0; + + if (!this.isFirstMonthFree) { + price = this.position?.netPrice ?? 0; + } + + return this.formatCurrency(price); + }, + + netPriceIntervalSnippet() { + if (this.isFirstMonthFree) { + return this.$t('sw-extension-store.purchase-confirmation.checkout.order.firstMonth'); + } + + return this.durationIntervalSnippet; + }, + + discountedPriceIntervalSnippet() { + let snippet = ''; + + const duration = this.position?.variant.duration; + + if (duration === 12) { + snippet += this.$t('sw-extension-store.purchase-confirmation.checkout.order.forFirstYear'); + } else { + const discountAppliesForMonths = this.position?.discountAppliesForMonths; + + snippet += this.$t('sw-extension-store.purchase-confirmation.checkout.order.forMonths', { + months: discountAppliesForMonths, + }); + } + + if (this.isFirstMonthFree) { + const price = this.formatCurrency(this.position?.netPrice ?? 0); + + if (duration === 12) { + snippet = this.$t('sw-extension-store.purchase-confirmation.checkout.order.followingPerYear', { + price: price, + }) + ' (' + snippet + ')'; + } else { + snippet = this.$t('sw-extension-store.purchase-confirmation.checkout.order.followingPerMonth', { + price: price, + }) + ' ' + snippet; + } + } + + return snippet; + }, + + pseudoPrice() { + return this.formatCurrency(this.position?.pseudoPrice ?? 0); + }, + + userCanBuyFromStore() { + return Shopware.Store.get('shopwareExtensions').userInfo !== null; + }, + + hasPaymentMethodError() { + return (this.paymentMeans || []).length <= 0 && + this.cart && this.cart.payment && this.cart.payment.paymentMeanRequired; + }, + + canConfirmPurchase() { + return this.userCanBuyFromStore + && this.tocAccepted + && (!this.extensionHasPermissionsOrDomains || this.permissionsAccepted) + && !this.hasPaymentMethodError; + }, + }, + + methods: { + formatCurrency(price: number) { + return Utils.format.currency(price, 'EUR', 2); + }, + + onTocAcceptedChange(value: boolean) { + this.$emit('update:toc-accepted', value); + }, + + onPermissionsAcceptedChange(value: boolean) { + this.$emit('update:permissions-accepted', value); + }, + + showPermissions() { + this.$emit('update:modal-view', 'permissions'); + }, + + closeModal() { + this.store.closeModal(); + }, + + cancelPurchase() { + this.store.cancel(); + }, + + async confirmPurchase() { + await this.store.confirm(); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.html.twig new file mode 100644 index 00000000..fadfde1a --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.html.twig @@ -0,0 +1,186 @@ +{% block sw_extension_store_purchase_confirmation_checkout %} +
+ + {% block sw_extension_store_purchase_confirmation_checkout_loader %} + + {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_success %} + + {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_failed %} + + {% endblock %} + + {% block sw_extension_store_purchase_confirmation_checkout_default %} + + {% endblock %} + +
+{% endblock %} + +{% block sw_extension_store_purchase_confirmation_footer %} + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.scss b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.scss new file mode 100644 index 00000000..e4558ee7 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout/sw-extension-store-purchase-confirmation-checkout.scss @@ -0,0 +1,112 @@ +.sw-extension-store-purchase-confirmation__checkout { + display: flex; + flex-direction: column; + gap: 24px; + padding: 24px; + position: relative; + + &-loader.mt-loader { + background-color: rgba(255, 255, 255, 0.65); + z-index: 10; + } + + &-success, + &-failed { + display: flex; + flex-direction: column; + gap: 24px; + + > .mt-banner, + > .mt-empty-state { + margin: 0; + max-width: none; + width: 100%; + } + } + + &-toc { + display: flex; + flex-direction: column; + gap: 12px; + + &-title { + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + font-weight: var(--font-weight-semibold); + } + + &-checkboxes { + color: var(--color-text-secondary-default); + display: flex; + flex-direction: column; + gap: 12px; + + &-item { + align-items: center; + display: flex; + gap: 4px; + + .mt-link { + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + .mt-field { + .mt-field__label { + color: var(--color-text-secondary-default); + margin-left: 8px; + } + } + } + } + } + + &-order { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 20px; + + &-summary { + &-net-price { + font-size: var(--font-size-m); + line-height: var(--font-line-height-m); + + &-value { + font-weight: var(--font-weight-bold); + } + } + + &-discounted-price, + &-pseudo-price { + color: var(--color-text-secondary-default); + font-size: var(--font-size-s); + line-height: var(--font-line-height-s); + } + } + } + + &-notes { + display: flex; + flex-direction: column; + gap: 4px; + + small { + color: var(--color-text-secondary-default); + font-size: var(--font-size-2xs); + line-height: var(--font-line-height-2xs); + } + } +} + +.sw-extension-store-purchase-confirmation__footer { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + justify-content: flex-end; + padding: var(--scale-size-24); +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts index c85eb80f..54b5b3ad 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/index.ts @@ -2,6 +2,8 @@ import { purchaseConfirmationStore } from '../../store/extension-store-purchase- import template from './sw-extension-store-purchase-confirmation-modal.html.twig'; import './sw-extension-store-purchase-confirmation-modal.scss'; +type ModalView = 'checkout' | 'permissions'; + /** * @private */ @@ -10,7 +12,10 @@ export default Shopware.Component.wrapComponentConfig({ data() { return { + view: 'checkout' as ModalView, store: purchaseConfirmationStore, + tocAccepted: false, + permissionsAccepted: false, }; }, @@ -23,18 +28,34 @@ export default Shopware.Component.wrapComponentConfig({ return this.store.state.isLoading; }, - cartData() { + cart() { return this.store.state.cartData; }, + + paymentMeans() { + return this.store.state.paymentMeansData; + }, + }, + + watch: { + isOpen(value: boolean) { + if (!value) { + this.resetModal(); + } + }, }, methods: { - async onConfirmPurchase() { - await this.store.confirm(); + resetModal() { + this.view = 'checkout'; + this.tocAccepted = false; + this.permissionsAccepted = false; }, - onCancelPurchase() { - this.store.cancel(); + onModalChange(value: boolean) { + if (!value) { + this.store.cancel(); + } }, }, }); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig index baddf71c..c414d25a 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.html.twig @@ -1,45 +1,32 @@ {% block sw_extension_store_purchase_confirmation_modal %} - - {% block sw_extension_store_purchase_confirmation_modal_content %} -
- {% block sw_extension_store_purchase_confirmation_modal_cart_data %} -
-

{{ $t('sw-extension-store.purchase-confirmation.description') }}

- - -
-
{{ JSON.stringify(cartData, null, 2) }}
-
+ + {% block sw_extension_store_purchase_confirmation_modal_content %} +
+ +
{% endblock %} -
- {% endblock %} - - {% block sw_extension_store_purchase_confirmation_modal_footer %} - - {% endblock %} - + + {% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss index 301fb62d..691fa1ef 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-modal/sw-extension-store-purchase-confirmation-modal.scss @@ -1,18 +1,19 @@ -.sw-extension-store-purchase-confirmation-modal { - &__content { - padding: 20px 0; +.mt-modal { + &__footer { + display: none; } +} - &__cart-data { - margin-bottom: 20px; +.sw-extension-store-purchase-confirmation-modal { + &__content { + display: flex; + flex-direction: column; } &__cart-info { background-color: #f5f5f5; - border: 1px solid #d1d5db; - border-radius: 4px; + border-bottom: 1px solid #d1d5db; padding: 16px; - margin-top: 12px; max-height: 400px; overflow-y: auto; @@ -23,4 +24,186 @@ word-wrap: break-word; } } + + &__checkout { + display: flex; + flex-direction: column; + gap: 24px; + padding: 24px; + + &-overview { + border: 1px solid var(--color-border-secondary-default); + border-radius: var(--border-radius-card); + display: flex; + flex-direction: column; + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + overflow: hidden; + + &-details { + align-items: center; + background: var(--color-background-secondary-default); + display: flex; + gap: 12px; + padding: 24px; + + &-icon { + border-radius: var(--border-radius-card); + height: 56px; + overflow: hidden; + width: 56px; + + &::after { + display: none; + } + } + + &-info { + &-name { + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-s); + } + + &-producer-name { + color: var(--color-text-secondary-default); + + a.mt-link { + color: var(--color-text-primary-default); + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + } + } + + &-info { + display: flex; + flex-direction: column; + padding: 8px 24px; + + &-row { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + flex-direction: column; + padding: 12px 0; + + &:first-child { + border-top: none; + } + + @media screen and (min-width: 768px) { + flex-direction: row; + } + + &-label { + color: var(--color-text-secondary-default); + + @media screen and (min-width: 768px) { + width: 180px; + } + } + + &-value { + a.mt-link { + color: var(--color-text-primary-default); + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + &-action { + margin-left: auto; + } + } + } + } + + &-toc { + display: flex; + flex-direction: column; + gap: 12px; + + &-title { + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + font-weight: var(--font-weight-semibold); + } + + &-checkboxes { + color: var(--color-text-secondary-default); + display: flex; + flex-direction: column; + gap: 12px; + + &-item { + align-items: center; + display: flex; + gap: 4px; + + a.mt-link { + font-weight: var(--font-weight-medium); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + .mt-field { + .mt-field__label { + color: var(--color-text-secondary-default); + margin-left: 8px; + } + } + } + } + } + + &-order { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 20px; + + &-summary { + &-net-price { + font-size: var(--font-size-m); + line-height: var(--font-line-height-m); + + &-value { + font-weight: var(--font-weight-bold); + } + } + + &-discounted-price, + &-pseudo-price { + color: var(--color-text-secondary-default); + font-size: var(--font-size-s); + line-height: var(--font-line-height-s); + } + } + } + + &-notes { + display: flex; + flex-direction: column; + gap: 4px; + + small { + color: var(--color-text-secondary-default); + font-size: var(--font-size-2xs); + line-height: var(--font-line-height-2xs); + } + } + } } diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/index.ts b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/index.ts new file mode 100644 index 00000000..78f3e321 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/index.ts @@ -0,0 +1,109 @@ +import template from './sw-extension-store-purchase-confirmation-permissions.html.twig'; +import './sw-extension-store-purchase-confirmation-permissions.scss'; + +import type { ExtensionStoreBasket } from '../../types/extension-store-basket.types'; + +/** + * @private + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + props: { + cart: { + type: Object as PropType, + required: true, + }, + }, + + emits: [ + 'update:modal-view', + ], + + computed: { + position() { + return this.cart?.positions?.[0]; + }, + + extension() { + return this.position?.extension; + }, + + operations() { + return [ + { + label: this.$tc('sw-extension-store.component.sw-extension-permissions-details-modal.operationRead'), + operation: 'read', + }, + { + label: this.$tc('sw-extension-store.component.sw-extension-permissions-details-modal.operationUpdate'), + operation: 'update', + }, + { + label: this.$tc('sw-extension-store.component.sw-extension-permissions-details-modal.operationCreate'), + operation: 'create', + }, + { + label: this.$tc('sw-extension-store.component.sw-extension-permissions-details-modal.operationDelete'), + operation: 'delete', + }, + ]; + }, + + permissions() { + const extensionPermissions = this.extension.permissions; + if (!extensionPermissions || Object.keys(extensionPermissions).length === 0) { + return null; + } + + return Object.fromEntries( + Object.entries(this.extension.permissions).map( + ([ + category, + permissions, + ]) => { + const grouped = permissions.reduce>((acc, permission) => { + const entity = permission.entity; + + if (entity === 'additional_privileges') { + acc[permission.operation] = []; + + return acc; + } + + acc[entity] = (acc[entity] || []).concat(permission.operation); + + return acc; + }, {}); + return [ + category, + grouped, + ]; + }, + ), + ); + }, + + domains() { + return this.extension.domains.filter((domain) => domain !== null); + }, + }, + + methods: { + categoryLabel(category: string) { + const translation = `entityCategories.${category}.title`; + + return this.$te(translation) ? this.$tc(translation) : category; + }, + + entityLabel(category: string, entity: string) { + const translation = `entityCategories.${category}.entities.${entity}`; + + return this.$te(translation) ? this.$tc(translation) : entity; + }, + + showCheckout() { + this.$emit('update:modal-view', 'checkout'); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.html.twig new file mode 100644 index 00000000..6137e33c --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.html.twig @@ -0,0 +1,159 @@ +{% block sw_extension_store_purchase_confirmation_permissions %} +
+ {% block sw_extension_store_purchase_confirmation_permissions_intro %} +
+
+
+ +
+
+ + {{ extension.label }} + + + {{ $t('sw-extension-store.purchase-confirmation.permissions.intro.label') }} + +
+
+
+

+ {{ $t('sw-extension-store.purchase-confirmation.permissions.intro.descriptionPermissions', { + extensionLabel: extension.label, + }) }} +

+
+
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_table %} + + {% block sw_extension_store_purchase_confirmation_permissions_table_head %} + + {% block sw_extension_store_purchase_confirmation_permissions_table_head_row %} + + + + {% endblock %} + + {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_table_body %} + + {% block sw_extension_store_purchase_confirmation_permissions_table_permissions %} + + {% endblock %} + + {% endblock %} +
+ + {{ operation.label }} +
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_domains %} +
+ {% block sw_extension_store_purchase_confirmation_permissions_domains_label %} +
+ {{ $t('sw-extension-store.purchase-confirmation.permissions.domains.label') }} +
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_domains_description %} +
+

{{ $t('sw-extension-store.purchase-confirmation.permissions.domains.description') }}

+
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_domains_list %} +
+
    + {% block sw_extension_domains_modal_list_entry %} +
  • + {{ domain }} +
  • + {% endblock %} +
+
+ {% endblock %} +
+ {% endblock %} + + {% block sw_extension_store_purchase_confirmation_permissions_footer %} + + {% endblock %} +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.scss b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.scss new file mode 100644 index 00000000..938bd118 --- /dev/null +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-permissions/sw-extension-store-purchase-confirmation-permissions.scss @@ -0,0 +1,189 @@ +.sw-extension-store-purchase-confirmation-permissions { + display: flex; + flex-direction: column; + + &__intro { + display: flex; + flex-direction: column; + gap: 24px; + padding: var(--scale-size-24); + + &-extension { + align-items: center; + display: flex; + gap: 12px; + + &-icon { + background-color: var(--color-background-brand-default); + border-radius: var(--border-radius-card); + color: var(--color-interaction-primary-pressed); + display: flex; + align-items: center; + justify-content: center; + height: 56px; + overflow: hidden; + width: 56px; + } + + &-title { + display: flex; + flex-direction: column; + + &-name { + color: var(--color-text-primary-default); + font-size: var(--font-size-m); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-m); + } + + &-label { + color: var(--color-text-secondary-default); + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + } + } + } + + &-description { + p { + color: var(--color-text-secondary-default); + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + } + } + } + + &__table { + background-color: var(--color-elevation-surface-sunken); + border-collapse: collapse; + border-spacing: unset; + border-top: 1px solid var(--color-border-secondary-default); + font-size: var(--font-size-xs); + + .sw-extension-store-purchase-confirmation-permissions__operations { + border-bottom: 1px solid var(--color-border-secondary-default); + display: block; + top: 0; + position: sticky; + background: #fff; + + tr { + &.sw-extension-store-purchase-confirmation-permissions__grid-columns { + padding: var(--scale-size-18) 24px var(--scale-size-18) 44px; + } + + td { + &.sw-extension-store-purchase-confirmation-permissions__operation-header { + color: var(--color-text-secondary-default); + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + text-align: center; + } + } + } + } + + tbody { + tr { + &:last-child { + padding-bottom: var(--scale-size-22); + } + } + } + + tr { + padding: 0 var(--scale-size-24); + + &.sw-extension-store-purchase-confirmation-permissions__category { + display: grid; + font-weight: var(--font-weight-bold); + background: var(--color-elevation-surface-sunken); + + &:not(:first-child) { + margin-top: var(--scale-size-22); + + td { + border-top: 1px solid var(--color-border-secondary-default); + } + } + + td { + padding: var(--scale-size-16) 0 var(--scale-size-6) 0; + } + } + + &.sw-extension-store-purchase-confirmation-permissions__grid-columns { + display: grid; + grid-template-columns: auto repeat(4, 80px); + padding: var(--scale-size-6) 24px var(--scale-size-6) 44px; + } + + td { + font-size: var(--font-size-s); + line-height: var(--font-line-height-s); + + &.sw-extension-store-purchase-confirmation-permissions__operation { + text-align: center; + + &.sw-extension-store-purchase-confirmation-permissions__operation-positive { + color: var(--color-icon-positive-default); + } + + &.sw-extension-store-purchase-confirmation-permissions__operation-negative { + color: var(--color-icon-secondary-default); + } + } + } + } + } + + &__domains { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + flex-direction: column; + gap: 12px; + padding: var(--scale-size-24); + + &-label { + color: var(--color-text-primary-default); + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-s); + } + + &-description { + p { + color: var(--color-text-secondary-default); + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + } + } + + &-list { + display: flex; + flex-direction: column; + gap: 12px; + + &-items { + list-style: none; + + li { + font-size: var(--font-size-xs); + line-height: var(--font-line-height-xs); + padding: var(--scale-size-10) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--color-border-secondary-default); + } + } + } + } + } + + &__footer { + border-top: 1px solid var(--color-border-secondary-default); + display: flex; + justify-content: center; + padding: var(--scale-size-24); + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/module/sw-extension-store/index.js b/src/Resources/app/administration/src/module/sw-extension-store/index.js index 6643a8b7..d93dc05b 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/index.js +++ b/src/Resources/app/administration/src/module/sw-extension-store/index.js @@ -10,7 +10,10 @@ Shopware.Component.register('sw-extension-store-index', () => import('./page/sw- Shopware.Component.register('sw-extension-store-slider', () => import('./component/sw-extension-store-slider')); Shopware.Component.register('sw-extension-store-listing-filter', () => import('./component/sw-extension-store-listing-filter')); Shopware.Component.register('sw-extension-buy-modal', () => import('./component/sw-extension-buy-modal')); +Shopware.Component.register('sw-extension-store-purchase-confirmation-checkout', () => import('./component/sw-extension-store-purchase-confirmation-checkout')); +Shopware.Component.register('sw-extension-store-purchase-confirmation-checkout-overview', () => import('./component/sw-extension-store-purchase-confirmation-checkout-overview')); Shopware.Component.register('sw-extension-store-purchase-confirmation-modal', () => import('./component/sw-extension-store-purchase-confirmation-modal')); +Shopware.Component.register('sw-extension-store-purchase-confirmation-permissions', () => import('./component/sw-extension-store-purchase-confirmation-permissions')); Shopware.Component.register('sw-extension-listing-card', () => import('./component/sw-extension-listing-card')); Shopware.Component.register('sw-extension-store-update-warning', () => import('./component/sw-extension-store-update-warning')); Shopware.Component.register('sw-extension-label', () => import('./component/sw-extension-store-label')); diff --git a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig index 1d119092..13d5767c 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/page/sw-extension-store-index/sw-extension-store-index.html.twig @@ -1,8 +1,8 @@ {% block sw_extension_store_index %} diff --git a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts index 83bf57f2..7d15361f 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/service/extension-store-channel.service.ts @@ -7,7 +7,7 @@ import type { ShopwareMessageTypes } from '@shopware-ag/meteor-admin-sdk/es/mess import { purchaseConfirmationStore } from '../store/extension-store-purchase-confirmation.store'; import type ExtensionHelperService from 'src/app/service/extension-helper.service'; import type CacheApiService from 'src/core/service/api/cache.api.service'; -import type { ExtensionStoreBasket } from '../types/extension-store-basket.types'; +import type { ExtensionStoreBasket, ExtensionStorePaymentMean } from '../types/extension-store-basket.types'; type StoreChannelAction = 'handshake' | 'routeTo' | 'purchase' | 'routerUpdate' | 'copyToClipboard'; @@ -208,15 +208,19 @@ export class ExtensionStoreChannelService { } private async handlePurchase(data: PurchaseActionData): Promise { - const cartData = (await this.extensionStoreLicensesService.newCart( - data.productId, - data.variantId, - )).data as ExtensionStoreBasket; + const [cartData, paymentMeansData] = await Promise.all([ + (await this.extensionStoreLicensesService.newCart( + data.productId, + data.variantId, + )).data as ExtensionStoreBasket, + (await this.extensionStoreLicensesService.getPaymentMeans()).data as ExtensionStorePaymentMean[], + ]); purchaseConfirmationStore.openModal( cartData, + paymentMeansData, async () => { - await this.performPurchase(data, cartData); + return await this.performPurchase(data, cartData); }, () => { this.publishPurchaseResult(data.sessionToken, false); @@ -273,17 +277,19 @@ export class ExtensionStoreChannelService { }); } - private async performPurchase(data: PurchaseActionData, cartData: ExtensionStoreBasket): Promise { + private async performPurchase(data: PurchaseActionData, cartData: ExtensionStoreBasket): Promise { try { await this.extensionStoreLicensesService.orderCart(cartData); this.publishPurchaseResult(data.sessionToken, true); } catch { this.publishPurchaseResult(data.sessionToken, false); - return; + return false; } await this.shopwareExtensionService.updateExtensionData(); await this.installExtension(cartData); + + return true; } private async installExtension(cartData: ExtensionStoreBasket): Promise { diff --git a/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json b/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json index 7e030e80..0eb6e557 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json +++ b/src/Resources/app/administration/src/module/sw-extension-store/snippet/de.json @@ -88,10 +88,79 @@ "onceOff": "einmalig" }, "purchase-confirmation": { + "backToCheckout": "Zurück zur Kasse", + "closeWindow": "Fenster schließen", "modalTitle": "Kauf bestätigen", - "description": "Bitte überprüfe die Kaufdetails vor der Bestätigung:", - "buyButton": "Kaufen", - "cancelButton": "Abbrechen" + "checkout": { + "success": { + "title": "Dein Kauf war erfolgreich!", + "description": "Eine Bestätigungsemail wurde an dich gesendet. Die Erweiterungen sind nun in deinem Konto und im Admin deines Shops sichtbar." + }, + "failed": { + "title": "Etwas ist schiefgelaufen", + "description": "Entschuldigung, dein Kauf konnte nicht abgeschlossen werden." + }, + "overview": { + "billingAddress": "Rechnungsadresse", + "bookingDomain": "Buchungsdomain", + "by": "von", + "change": "ändern", + "changeBillingAddressUrl": "https://account.shopware.com/settings/data", + "paymentMethod": "Zahlungsmethode", + "noPaymentMethod": "Keine Zahlungsmethode gespeichert", + "addPaymentMethod": "Zahlungsmethode hinzufügen", + "paymentMethodBanner": { + "noPaymentMethod": { + "title": "Bitte füge eine Zahlungsmethode hinzu, um fortzufahren", + "description": "Um Erweiterungen in unserem Store zu kaufen, ist eine Zahlungsmethode erforderlich. Bitte füge eine Zahlungsmethode in deinem Konto hinzu. Um sie hinzuzufügen, klicke auf den Button unten.", + "action": "Zahlungsmethode hinzufügen" + }, + "change": { + "title": "Zahlungsmethode ändern?", + "description": "Die standardmäßige Zahlungsmethode, die in deinem Konto gespeichert ist, wird für diesen Kauf verwendet. Um sie zu ändern, klicke auf den Button unten.

Hinweis: Nach dem Aktualisieren der standardmäßigen Zahlungsmethode in deinem Konto wird sie im Hintergrund aktualisiert und automatisch verwendet, wenn du diese Bestellung absendest. Kein Neuladen erforderlich.", + "action": "Zahlungsmethode ändern" + } + } + }, + "toc": { + "title": "Nutzungsbedingungen", + "checkboxes": { + "gtc": { + "text": "Ich habe die {link} gelesen und stimme ihnen zu.", + "link": "Allgemeine Nutzungsbedingungen" + }, + "permissions": { + "text": "Ich bin mir bewusst, dass diese Erweiterung weitere Berechtigungen benötigt.", + "link": "Berechtigungen anzeigen" + } + } + }, + "order": { + "firstMonth": "/1. Monat", + "followingPerMonth": "Anschließend {price}** /Monat", + "followingPerYear": "Anschließend {price}** /Jahr", + "forMonths": "für {months} Monate", + "forFirstYear": "für das erste Jahr", + "perMonth": "/Monat", + "perYear": "/Jahr", + "submit": "Bestellung absenden", + "then": "Dann" + }, + "notes": { + "tax": "* Alle Preise verstehen sich ohne die gesetzliche Mehrwertsteuer" + } + }, + "permissions": { + "intro": { + "label": "Berechtigungen", + "descriptionPermissions": "{extensionLabel} wird Zugriff auf die Komponenten unten erhalten.", + "descriptionDomains": "Einige Daten werden auch mit den Domains unten geteilt." + }, + "domains": { + "label": "Domains", + "description": "Einige Daten werden auch mit den folgenden Domains geteilt." + } + } }, "installation": { "successTitle": "Installation erfolgreich", diff --git a/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json b/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json index 2f6d6844..9f75820a 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json +++ b/src/Resources/app/administration/src/module/sw-extension-store/snippet/en.json @@ -88,10 +88,79 @@ "onceOff": "one-time" }, "purchase-confirmation": { - "modalTitle": "Confirm Purchase", - "description": "Please review your purchase details before confirming:", - "buyButton": "Buy", - "cancelButton": "Cancel" + "backToCheckout": "Back to checkout", + "closeWindow": "Close window", + "modalTitle": "Checkout", + "checkout": { + "success": { + "title": "Your order was successful!", + "description": "Order confirmation email has been sent. The extensions are now visible in your Account and in the Admin of your shop." + }, + "failed": { + "title": "Something went wrong", + "description": "Sorry, your purchase could not be completed." + }, + "overview": { + "billingAddress": "Billing address", + "bookingDomain": "Booking domain", + "by": "by", + "change": "Change", + "changeBillingAddressUrl": "https://account.shopware.com/settings/data", + "paymentMethod": "Payment method", + "noPaymentMethod": "No payment method stored", + "addPaymentMethod": "Add payment method", + "paymentMethodBanner": { + "noPaymentMethod": { + "title": "Please provide a payment method to proceed", + "description": "For licensing Extensions in our store a payment method is required. Please add a payment method in your Account. To add it, click the button below.", + "action": "Add payment method" + }, + "change": { + "title": "Change payment method?", + "description": "The standard payment method that has been added to the Account will be used for this order. To change it, click the button below.

Note: After updating the standard payment method in the Account it will be updated in the background and automatically be used if you submit this order. No reload required.", + "action": "Change payment method" + } + } + }, + "toc": { + "title": "Terms and Conditions", + "checkboxes": { + "gtc": { + "text": "I have read the {link} and agree with them.", + "link": "General Terms and Conditions" + }, + "permissions": { + "text": "I am aware that this extension needs additional permissions.", + "link": "Show permissions" + } + } + }, + "order": { + "firstMonth": "/first month", + "followingPerMonth": "Following {price}** /month", + "followingPerYear": "Following {price}** /year", + "forMonths": "for {months} months", + "forFirstYear": "for the first year", + "perMonth": "/month", + "perYear": "/year", + "submit": "Submit order", + "then": "Then" + }, + "notes": { + "tax": "* All prices are quoted net of the statutory value-added tax" + } + }, + "permissions": { + "intro": { + "label": "Permissions", + "descriptionPermissions": "{extensionLabel} will get access to the components below.", + "descriptionDomains": "Some data will also be shared with the domains listed below." + }, + "domains": { + "label": "Domains", + "description": "Some data will be shared with / transferred to the domains listed below." + } + } }, "installation": { "successTitle": "Installation successful", diff --git a/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts b/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts index 8414a018..b8463dca 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-purchase-confirmation.store.ts @@ -1,13 +1,16 @@ import { reactive } from 'vue'; -import type { ExtensionStoreBasket } from '../types/extension-store-basket.types'; +import type { ExtensionStoreBasket, ExtensionStorePaymentMean } from '../types/extension-store-basket.types'; -type OnConfirmCallback = () => Promise; +type OnConfirmCallback = () => Promise; type OnCancelCallback = () => void; type PurchaseConfirmationState = { isOpen: boolean; isLoading: boolean; + isSubmitted: boolean; + isSuccessful: boolean; cartData: ExtensionStoreBasket | null; + paymentMeansData: ExtensionStorePaymentMean[] | null; onConfirm: OnConfirmCallback | null; onCancel: OnCancelCallback | null; }; @@ -15,7 +18,10 @@ type PurchaseConfirmationState = { const state = reactive({ isOpen: false, isLoading: false, + isSubmitted: false, + isSuccessful: false, cartData: null, + paymentMeansData: null, onConfirm: null, onCancel: null, }); @@ -27,10 +33,12 @@ export const purchaseConfirmationStore = { openModal( cartResponse: ExtensionStoreBasket, + paymentMeansResponse: ExtensionStorePaymentMean[], onConfirm: OnConfirmCallback, onCancel: OnCancelCallback, ): void { state.cartData = cartResponse; + state.paymentMeansData = paymentMeansResponse; state.onConfirm = onConfirm; state.onCancel = onCancel; state.isOpen = true; @@ -41,13 +49,19 @@ export const purchaseConfirmationStore = { return; } + state.isSubmitted = true; state.isLoading = true; - try { - await state.onConfirm(); - } finally { - this.closeModal(); + const success = await state.onConfirm(); + + state.isSuccessful = success ?? false; + + if (success) { + state.onCancel = null; + state.onConfirm = null; } + + state.isLoading = false; }, cancel(): void { @@ -60,7 +74,10 @@ export const purchaseConfirmationStore = { closeModal(): void { state.isOpen = false; state.isLoading = false; + state.isSubmitted = false; + state.isSuccessful = false; state.cartData = null; + state.paymentMeansData = null; state.onConfirm = null; state.onCancel = null; }, diff --git a/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts b/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts index 8a669ff9..f5071eb5 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts +++ b/src/Resources/app/administration/src/module/sw-extension-store/types/extension-store-basket.types.ts @@ -1,12 +1,37 @@ +type ExtensionStoreBasketBillingAddress = { + companyName: string; + fullName?: string; + street: string; + zip: string; + city: string; + country: string; +}; + type ExtensionStoreBasketShop = { id: number; domain: string; }; +type ExtensionStoreBasketExtensionProducer = { + id: number; + name: string; + storeLink?: string; +}; + +type ExtensionStoreBasketExtensionPermission = { + entity: string; + operation: string; +}; + type ExtensionStoreBasketExtension = { id: number; name: string; type: 'app' | 'plugin'; + icon: string; + label: string; + producer: ExtensionStoreBasketExtensionProducer; + permissions: Record; + domains: string[]; }; type ExtensionStoreBasketVariant = { @@ -33,11 +58,13 @@ type ExtensionStoreBasketPayment = { chargingAmount: number; paymentMean: { id: number } | null; paymentText: string | null; + paymentTextLabel: string | null; paymentMeanRequired: boolean; registrationUrl: string; }; type ExtensionStoreBasket = { + billingAddress: ExtensionStoreBasketBillingAddress; bookingShop: ExtensionStoreBasketShop & { balance: number }; licenseShop: ExtensionStoreBasketShop; /** @deprecated use licenseShop - kept for backwards compatibility */ @@ -51,6 +78,13 @@ type ExtensionStoreBasket = { legalText: string | null; }; +type ExtensionStorePaymentMean = { + id: number; + type: string; + label: string; + default: boolean; +}; + export type { ExtensionStoreBasketShop, ExtensionStoreBasketExtension, @@ -58,4 +92,5 @@ export type { ExtensionStoreBasketPosition, ExtensionStoreBasketPayment, ExtensionStoreBasket, + ExtensionStorePaymentMean, }; diff --git a/src/Services/LicenseService.php b/src/Services/LicenseService.php index af6563f2..86011df2 100644 --- a/src/Services/LicenseService.php +++ b/src/Services/LicenseService.php @@ -6,7 +6,9 @@ use Shopware\Core\Framework\Context; use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\Store\Struct\CartPositionStruct; use Shopware\Core\Framework\Store\Struct\CartStruct; +use Shopware\Core\Framework\Store\Struct\PermissionCollection; use Shopware\Core\Framework\Util\FloatComparator; /** @@ -26,7 +28,7 @@ public function createCart(int|string $extensionId, int|string $variantId, Conte { $cart = $this->client->createCart($extensionId, $variantId, $context); - return $this->ensureFloatPrecision($cart); + return $this->ensureCartStruct($cart); } public function orderCart(CartStruct $cart, Context $context): void @@ -42,18 +44,31 @@ public function availablePaymentMeans(Context $context): array return $this->client->availablePaymentMeans($context); } - protected function ensureFloatPrecision(CartStruct $cart): CartStruct + protected function ensureCartStruct(CartStruct $cart): CartStruct { + // Ensure float precision $cart->setGrossPrice(FloatComparator::cast($cart->getGrossPrice())); $cart->setNetPrice(FloatComparator::cast($cart->getNetPrice())); $cart->setTaxValue(FloatComparator::cast($cart->getTaxValue())); $cart->setTaxRate(FloatComparator::cast($cart->getTaxRate())); + /** @var CartPositionStruct $position */ foreach ($cart->getPositions() as $position) { + // Ensure float precision per position $position->setGrossPrice(FloatComparator::cast($position->getGrossPrice())); $position->setNetPrice(FloatComparator::cast($position->getNetPrice())); $position->setPseudoPrice(FloatComparator::cast($position->getPseudoPrice())); $position->setTaxValue(FloatComparator::cast($position->getTaxValue())); + + // Ensure extension information is initialized correctly + $extensionInformation = $position->getExtensionInformation(); + + if (isset($extensionInformation['permissions'])) { + $permissions = new PermissionCollection($extensionInformation['permissions']); + $extensionInformation['permissions'] = $permissions->getCategorizedPermissions(); + } + + $position->setExtensionInformation($extensionInformation); } return $cart; From 52678e082bc2b7fc895967d02c19cf14cd86e8b6 Mon Sep 17 00:00:00 2001 From: Jannik <43863063+qysp@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:10:34 +0200 Subject: [PATCH 09/38] ECTO-2581 - Add tracking for extension store checkout (#144) --- .../app/administration/jest.config.js | 3 +- .../app/administration/package-lock.json | 27 ++++- src/Resources/app/administration/package.json | 2 +- .../app/administration/src/global.types.ts | 2 + ...e-confirmation-checkout-overview.html.twig | 10 +- ...e-purchase-confirmation-checkout.html.twig | 3 +- .../page/sw-extension-store-index/index.js | 3 + .../extension-store-channel.service.ts | 104 ++++++++++++++---- .../store/extension-store-context.store.ts | 17 +++ ...nsion-store-purchase-confirmation.store.ts | 33 ++++++ .../app/administration/src/util/telemetry.ts | 29 +++++ .../app/administration/src/vite.config.ts | 28 +++++ 12 files changed, 227 insertions(+), 34 deletions(-) create mode 100644 src/Resources/app/administration/src/module/sw-extension-store/store/extension-store-context.store.ts create mode 100644 src/Resources/app/administration/src/util/telemetry.ts diff --git a/src/Resources/app/administration/jest.config.js b/src/Resources/app/administration/jest.config.js index bb2a01cc..6afd3c42 100644 --- a/src/Resources/app/administration/jest.config.js +++ b/src/Resources/app/administration/jest.config.js @@ -10,7 +10,8 @@ module.exports = { preset: '@shopware-ag/jest-preset-sw6-admin', globals: { // required, e.g. /www/sw6/platform/src/Administration/Resources/app/administration - adminPath + adminPath, + __SWAG_EXTENSION_STORE_VERSION__: '0.0.0', }, setupFilesAfterEnv: [ diff --git a/src/Resources/app/administration/package-lock.json b/src/Resources/app/administration/package-lock.json index 9db0d247..ed8327d8 100644 --- a/src/Resources/app/administration/package-lock.json +++ b/src/Resources/app/administration/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@shopware-ag/meteor-admin-sdk": "^6.4.0" + "@shopware-ag/meteor-admin-sdk": "^6.8.0" }, "devDependencies": { "@babel/preset-env": "^7.20.2", @@ -3780,13 +3780,14 @@ } }, "node_modules/@shopware-ag/meteor-admin-sdk": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@shopware-ag/meteor-admin-sdk/-/meteor-admin-sdk-6.4.0.tgz", - "integrity": "sha512-Pyj6/LAhFNqlG5BWHHQRoyYignUTLCFZrK0+XTGJ5XRr2eQP1MhZg8Kn0zxnK26OclL4a23x2dDgaIki73AOig==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@shopware-ag/meteor-admin-sdk/-/meteor-admin-sdk-6.8.0.tgz", + "integrity": "sha512-BZDTd1i4VxWTtyn2k8yMspho4T1dI9WGQI7j/msp7+LMo/G+Ly5wjryKxt5MwqtD/50/i5jLaUtimToDLM4lfA==", "license": "MIT", "dependencies": { + "jwt-decode": "^4.0.0", "localforage": "^1.10.0", - "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "semver": "^7.7.1" } }, @@ -10093,6 +10094,15 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10179,6 +10189,13 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.debounce": { diff --git a/src/Resources/app/administration/package.json b/src/Resources/app/administration/package.json index 639e5229..c3250163 100644 --- a/src/Resources/app/administration/package.json +++ b/src/Resources/app/administration/package.json @@ -32,6 +32,6 @@ "administration": "^6.x" }, "dependencies": { - "@shopware-ag/meteor-admin-sdk": "^6.4.0" + "@shopware-ag/meteor-admin-sdk": "^6.8.0" } } diff --git a/src/Resources/app/administration/src/global.types.ts b/src/Resources/app/administration/src/global.types.ts index 26adfc81..497b3da3 100644 --- a/src/Resources/app/administration/src/global.types.ts +++ b/src/Resources/app/administration/src/global.types.ts @@ -10,4 +10,6 @@ declare global { } type ErrorResponse = AxiosError<{ errors: Array }>; + + const __SWAG_EXTENSION_STORE_VERSION__: string; } diff --git a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig index 5a1078c0..2c9ee4b6 100644 --- a/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig +++ b/src/Resources/app/administration/src/module/sw-extension-store/component/sw-extension-store-purchase-confirmation-checkout-overview/sw-extension-store-purchase-confirmation-checkout-overview.html.twig @@ -18,6 +18,7 @@ target="_blank" type="external" as="a" + data-analytics-source="SwagExtensionStore" > {{ producer.name }} @@ -43,6 +44,7 @@ target="_blank" type="external" as="a" + data-analytics-source="SwagExtensionStore" > {{ bookingShop.domain }} @@ -77,6 +79,7 @@ target="_blank" type="external" :to="$t('sw-extension-store.purchase-confirmation.checkout.overview.changeBillingAddressUrl')" + data-analytics-source="SwagExtensionStore" > {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.change') }} @@ -122,13 +125,14 @@ @click="showPaymentMethodBanner = !showPaymentMethodBanner" > {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.change') }} - + {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.addPaymentMethod') }} @@ -151,6 +155,7 @@ is="a" target="_blank" :link="payment.registrationUrl" + data-analytics-source="SwagExtensionStore" > {{ $t('sw-extension-store.purchase-confirmation.checkout.overview.paymentMethodBanner.noPaymentMethod.action') }}