diff --git a/src/Resources/Schema/AdminApi/openapi.json b/src/Resources/Schema/AdminApi/openapi.json index 3821b3cef..3fb1770ca 100644 --- a/src/Resources/Schema/AdminApi/openapi.json +++ b/src/Resources/Schema/AdminApi/openapi.json @@ -1368,6 +1368,42 @@ } } }, + "/api/_action/paypal/test-api-credentials": { + "post": { + "tags": [ + "Admin Api", + "PayPal" + ], + "operationId": "testApiCredentials", + "responses": { + "200": { + "description": "Returns if the provided API credentials are valid", + "content": { + "application/json": { + "schema": { + "required": [ + "valid", + "errors" + ], + "properties": { + "valid": { + "type": "boolean" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/error" + } + } + }, + "type": "object" + } + } + } + } + } + } + }, "/api/_action/paypal/get-api-credentials": { "post": { "tags": [ @@ -1448,6 +1484,30 @@ } } }, + "/api/_action/paypal/save-settings": { + "post": { + "tags": [ + "Admin Api", + "PayPal" + ], + "operationId": "saveSettings", + "responses": { + "200": { + "description": "Returns information about the saved settings", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/swag_paypal_setting_settings_information" + } + } + } + } + } + } + } + }, "/api/_action/paypal/webhook/status/{salesChannelId}": { "get": { "tags": [ @@ -6868,7 +6928,12 @@ ], "properties": { "merchantIntegrations": { - "$ref": "#/components/schemas/swag_paypal_v1_merchant_integrations" + "oneOf": [ + { + "$ref": "#/components/schemas/swag_paypal_v1_merchant_integrations" + } + ], + "nullable": true }, "capabilities": { "description": "string> key: paymentMethodId, value: capability (see AbstractMethodData)", @@ -6879,6 +6944,38 @@ } }, "type": "object" + }, + "swag_paypal_setting_settings_information": { + "required": [ + "sandboxCredentialsChanged", + "sandboxCredentialsValid", + "liveCredentialsChanged", + "liveCredentialsValid", + "webhookErrors" + ], + "properties": { + "sandboxCredentialsChanged": { + "type": "boolean" + }, + "sandboxCredentialsValid": { + "type": "boolean", + "nullable": true + }, + "liveCredentialsChanged": { + "type": "boolean" + }, + "liveCredentialsValid": { + "type": "boolean", + "nullable": true + }, + "webhookErrors": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object" } } }, diff --git a/src/Resources/app/administration/eslint.config.mjs b/src/Resources/app/administration/eslint.config.mjs index 3ab4cdae9..a445df4d6 100644 --- a/src/Resources/app/administration/eslint.config.mjs +++ b/src/Resources/app/administration/eslint.config.mjs @@ -190,6 +190,7 @@ export default tseslint.config( 'jest/prefer-to-contain': 'error', 'jest/prefer-to-have-length': 'error', 'jest/consistent-test-it': ['error', { fn: 'it', withinDescribe: 'it' }], + '@typescript-eslint/no-unsafe-member-access': 'off', // Needed for any/VueComponent typed wrappers }, }, { diff --git a/src/Resources/app/administration/jest.setup.js b/src/Resources/app/administration/jest.setup.js index 25d9e18b2..bede15f49 100644 --- a/src/Resources/app/administration/jest.setup.js +++ b/src/Resources/app/administration/jest.setup.js @@ -2,3 +2,14 @@ import 'SwagPayPal/mixin/swag-paypal-credentials-loader.mixin'; import 'SwagPayPal/mixin/swag-paypal-notification.mixin'; import 'SwagPayPal/mixin/swag-paypal-pos-catch-error.mixin'; import 'SwagPayPal/mixin/swag-paypal-pos-log-label.mixin'; + +import 'SwagPayPal/mixin/swag-paypal-settings.mixin'; +import 'SwagPayPal/mixin/swag-paypal-merchant-information.mixin'; + +import 'SwagPayPal/app/store/swag-paypal-settings.store'; +import 'SwagPayPal/app/store/swag-paypal-merchant-information.store'; + +afterEach(() => { + Shopware.Store.get('swagPayPalSettings').$reset(); + Shopware.Store.get('swagPayPalMerchantInformation').$reset(); +}); diff --git a/src/Resources/app/administration/src/app/acl/index.ts b/src/Resources/app/administration/src/app/acl/index.ts new file mode 100644 index 000000000..9a7ba0fc0 --- /dev/null +++ b/src/Resources/app/administration/src/app/acl/index.ts @@ -0,0 +1,66 @@ +Shopware.Service('privileges').addPrivilegeMappingEntry({ + category: 'permissions', + parent: 'swag_paypal', + key: 'swag_paypal', + roles: { + viewer: { + privileges: [ + 'sales_channel:read', + 'sales_channel_payment_method:read', + 'system_config:read', + ], + dependencies: [], + }, + editor: { + privileges: [ + 'sales_channel:update', + 'sales_channel_payment_method:create', + 'sales_channel_payment_method:update', + 'system_config:update', + 'system_config:create', + 'system_config:delete', + ], + dependencies: [ + 'swag_paypal.viewer', + ], + }, + }, +}); + +Shopware.Service('privileges').addPrivilegeMappingEntry({ + category: 'permissions', + parent: null, + key: 'sales_channel', + roles: { + viewer: { + privileges: [ + 'swag_paypal_pos_sales_channel:read', + 'swag_paypal_pos_sales_channel_run:read', + 'swag_paypal_pos_sales_channel_run:update', + 'swag_paypal_pos_sales_channel_run:create', + 'swag_paypal_pos_sales_channel_run_log:read', + 'sales_channel_payment_method:read', + ], + }, + editor: { + privileges: [ + 'swag_paypal_pos_sales_channel:update', + 'swag_paypal_pos_sales_channel_run:delete', + 'payment_method:update', + ], + }, + creator: { + privileges: [ + 'swag_paypal_pos_sales_channel:create', + 'payment_method:create', + 'shipping_method:create', + 'delivery_time:create', + ], + }, + deleter: { + privileges: [ + 'swag_paypal_pos_sales_channel:delete', + ], + }, + }, +}); diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/index.ts b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/index.ts new file mode 100644 index 000000000..07815945a --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/index.ts @@ -0,0 +1,292 @@ +import template from './swag-paypal-onboarding-button.html.twig'; +import './swag-paypal-onboarding-button.scss'; + +/** + * @private - The component has a stable public API (props), but expect that implementation details may change. + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + 'SwagPayPalSettingsService', + ], + + emits: ['onboarded'], + + mixins: [ + Shopware.Mixin.getByName('notification'), + ], + + props: { + type: { + type: String as PropType<'live' | 'sandbox'>, + required: false, + default: 'live', + }, + variant: { + type: String as PropType<'ghost' | 'link'>, + required: false, + default: 'ghost', + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + }, + + data() { + return { + // Will allow local overrides as props are readonly. + // Note that this is overridden if the prop changes. + type: this.$props.type, + + callbackId: Shopware.Utils.createId(), + + isLoading: true, + + scriptId: 'paypal-js', + scriptURL: 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js', + + live: { + partnerId: 'DYKPBPEAW5JNA', + partnerClientId: 'AR1aQ13lHxH1c6b3CDd8wSY6SWad2Lt5fv5WkNIZg-qChBoGNfHr2kT180otUmvE_xXtwkgahXUBBurW', + sellerNonce: `${Shopware.Utils.createId()}${Shopware.Utils.createId()}`, + }, + sandbox: { + partnerId: '45KXQA7PULGAG', + partnerClientId: 'AQ9g8qMYHpE8s028VCq_GO3Roy9pjeqGDjKTkR_sxzX0FtncBb3QUWbFtoQMtdpe2lG9NpnDT419dK8s', + sellerNonce: `${Shopware.Utils.createId()}${Shopware.Utils.createId()}`, + }, + commonRequestParams: { + channelId: 'partner', + product: 'ppcp', + secondaryProducts: 'advanced_vaulting,PAYMENT_METHODS', + capabilities: [ + 'APPLE_PAY', + 'GOOGLE_PAY', + 'PAY_UPON_INVOICE', + 'PAYPAL_WALLET_VAULTING_ADVANCED', + ].join(','), + integrationType: 'FO', + features: [ + 'PAYMENT', + 'REFUND', + 'READ_SELLER_DISPUTE', + 'UPDATE_SELLER_DISPUTE', + 'ADVANCED_TRANSACTIONS_SEARCH', + 'ACCESS_MERCHANT_INFORMATION', + 'TRACKING_SHIPMENT_READWRITE', + 'VAULT', + 'BILLING_AGREEMENT', + ].join(','), + displayMode: 'minibrowser', + partnerLogoUrl: 'https://assets.shopware.com/media/logos/shopware_logo_blue.svg', + }, + }; + }, + + watch: { + '$props.type'() { + this.type = this.$props.type; + }, + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + merchantInformationStore() { + return Shopware.Store.get('swagPayPalMerchantInformation'); + }, + + isSandbox() { + return this.type === 'sandbox'; + }, + + suffix() { + return this.isSandbox ? 'Sandbox' : ''; + }, + + returnUrl(): string { + return `${window.location.origin}${window.location.pathname}#${this.$route.path}?ppOnboarding=${this.type}`; + }, + + requestParams() { + return this.isSandbox ? this.sandbox : this.live; + }, + + onboardingUrl() { + const url = new URL('/bizsignup/partner/entry', this.isSandbox ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'); + + url.search = (new URLSearchParams({ + ...this.commonRequestParams, + ...this.requestParams, + returnToPartnerUrl: this.returnUrl, + })).toString(); + + return url.href; + }, + + buttonTitle(): string { + if (!this.settingsStore.get(`SwagPayPal.settings.clientSecret${this.suffix}`)) { + return this.$t(`swag-paypal-onboarding-button.${this.type}.title`); + } + + if (!this.merchantInformationStore.canPPCP) { + return this.$t(`swag-paypal-onboarding-button.${this.type}.onboardingTitle`); + } + + return this.$t(`swag-paypal-onboarding-button.${this.type}.changeTitle`); + }, + + callbackName(): `onboardingCallback${string}` { + return `onboardingCallback${this.callbackId}`; + }, + + isDisabled() { + return !this.acl.can('swag_paypal.editor') || this.isLoading || this.disabled; + }, + + classes() { + return { + 'is--sandbox': this.isSandbox, + 'is--live': !this.isSandbox, + 'is--link': this.variant === 'link', + 'sw-button': this.variant === 'ghost', + 'sw-button--ghost': this.variant === 'ghost', + 'is--disabled': this.isDisabled, + }; + }, + }, + + mounted() { + this.onMounted(); + }, + + beforeUnmount() { + delete window[this.callbackName]; + }, + + methods: { + onMounted() { + if (!this.acl.can('swag_paypal.editor')) { + return; + } + + if (Object.hasOwn(this.$route.query, 'ppOnboarding')) { + this.completeOnboarding(); + } + + window[this.callbackName] = (authCode, sharedId) => { + this.fetchCredentials(authCode, sharedId); + }; + + this.loadPayPalScript(); + }, + + createScriptElement(): HTMLScriptElement { + const payPalScript = document.createElement('script'); + payPalScript.id = this.scriptId; + payPalScript.type = 'text/javascript'; + payPalScript.src = this.scriptURL; + payPalScript.async = true; + + document.head.appendChild(payPalScript); + + return payPalScript; + }, + + loadPayPalScript() { + const el = document.getElementById(this.scriptId) ?? this.createScriptElement(); + + if (window.PAYPAL) { + this.isLoading = false; + window.PAYPAL.apps.Signup.setup(); + } else { + el.addEventListener('load', this.renderPayPalButton.bind(this), false); + } + }, + + renderPayPalButton() { + this.isLoading = false; + + // The original render function inside the partner.js is overwritten here. + // The function gets overwritten again, as soon as PayPals signup.js is loaded. + // A loop is created and the render() function is executed until the real render() function is available. + // PayPal does originally nearly the same, but only once and not in a loop. + // If the signup.js is loaded to slow the button is not rendered. + window.PAYPAL!.apps.Signup.render = function proxyPPrender() { + if (window.PAYPAL!.apps.Signup.timeout) { + clearTimeout(window.PAYPAL!.apps.Signup.timeout); + } + + window.PAYPAL!.apps.Signup.timeout = setTimeout(window.PAYPAL!.apps.Signup.render, 300); + }; + + window.PAYPAL!.apps.Signup.render(); + }, + + async fetchCredentials(authCode: string, sharedId: string) { + if (this.isLoading) { + return; + } + + this.isLoading = true; + + const response = await this.SwagPayPalSettingsService.getApiCredentials( + authCode, + sharedId, + this.requestParams.sellerNonce, + this.isSandbox, + ).catch(() => { + this.createNotificationError({ + message: this.$t('swag-paypal.settingForm.credentials.button.messageFetchedError'), + // @ts-expect-error - wrongly typed as string + duration: 10000, + }); + + return {} as Record; + }); + + this.setConfig(response.client_id, response.client_secret, response.payer_id); + + this.isLoading = false; + }, + + setConfig(clientId?: string, clientSecret?: string, merchantPayerId?: string) { + this.settingsStore.set(`SwagPayPal.settings.clientId${this.suffix}`, clientId); + this.settingsStore.set(`SwagPayPal.settings.clientSecret${this.suffix}`, clientSecret); + this.settingsStore.set(`SwagPayPal.settings.merchantPayerId${this.suffix}`, merchantPayerId); + + // First time onboarding + if (!this.merchantInformationStore.canPPCP) { + this.settingsStore.set('SwagPayPal.settings.sandbox', this.isSandbox); + } + + this.$emit('onboarded'); + }, + + completeOnboarding() { + const { ppOnboarding, merchantIdInPayPal } = this.$route.query; + this.$router.replace({ query: {} }); + + if (!merchantIdInPayPal || ppOnboarding !== 'sandbox' && ppOnboarding !== 'live') { + return; + } + + const suffix = ppOnboarding === 'sandbox' ? 'Sandbox' : ''; + const merchantPayerId = String(merchantIdInPayPal); + this.settingsStore.set( + `SwagPayPal.settings.merchantPayerId${suffix}`, + merchantPayerId, + ); + + this.$emit('onboarded'); + }, + }, +}); diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.html.twig b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.html.twig new file mode 100644 index 000000000..40c35907d --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.html.twig @@ -0,0 +1,19 @@ + + + + + {{ buttonTitle }} + + diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.scss b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.scss new file mode 100644 index 000000000..446afdb2c --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.scss @@ -0,0 +1,34 @@ +@import "~scss/variables"; + +.swag-paypal-onboarding-button { + display: flex; + align-items: center; + gap: 12px; + + &.sw-button { + text-overflow: ellipsis; + overflow: hidden; + } + + &.is--link { + font-weight: $font-weight-semi-bold; + font-size: $font-size-xs; + align-self: center; + text-wrap: balance; + } + + // :not will prioritize this rule higher + &.is--disabled:not(.mt-button) { + cursor: not-allowed; + color: $color-shopware-brand-200; + border-color: $color-shopware-brand-200; + + &::after { + background-color: $color-shopware-brand-200; + } + } +} + +.isu-minibrowser-component { + z-index: 2000; // above all shopware components (like modals) +} diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.spec.ts b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.spec.ts new file mode 100644 index 000000000..92e88decd --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-onboarding-button/swag-paypal-onboarding-button.spec.ts @@ -0,0 +1,231 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalOnboardingButton from '.'; + +Shopware.Component.register('swag-paypal-onboarding-button', Promise.resolve(SwagPayPalOnboardingButton)); + +async function loadScript(script: Element) { + window.PAYPAL = { + apps: { + // @ts-expect-error - not fully implemented on purpose + Signup: { + setup: jest.fn(), + render: jest.fn(), + }, + }, + }; + + script.dispatchEvent(new Event('load')); + + await flushPromises(); +} + +async function createWrapper() { + const route: Record = { + path: '/sw/path', + query: {}, + }; + + return mount( + await Shopware.Component.build('swag-paypal-onboarding-button') as typeof SwagPayPalOnboardingButton, + { + global: { + mocks: { + $t: (key: string) => key, + $route: route, + $router: { + replace: jest.fn((params: Record) => { + route.query = params.query; + }), + }, + }, + provide: { + acl: { can: () => true }, + SwagPayPalSettingsService: { + getApiCredentials: jest.fn(), + }, + }, + }, + }, + ); +} + +describe('swag-paypal-onboarding-button', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should initialize component', async () => { + const wrapper = await createWrapper(); + + const script = document.head.querySelector('#paypal-js'); + expect(script).toBeTruthy(); + await loadScript(script!); + + expect(typeof window.PAYPAL!.apps.Signup.render).toBe('function'); + expect(window.PAYPAL!.apps.Signup.setup).not.toHaveBeenCalled(); + + const renderSpy = jest.spyOn(wrapper.vm, 'renderPayPalButton'); + + wrapper.vm.loadPayPalScript(); + + expect(renderSpy).not.toHaveBeenCalled(); + expect(window.PAYPAL!.apps.Signup.setup).toHaveBeenCalled(); + }); + + it('should set config', async () => { + const wrapper = await createWrapper(); + const store = Shopware.Store.get('swagPayPalSettings'); + + store.setConfig(null, {}); + wrapper.setProps({ type: 'live' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.suffix).toBe(''); + wrapper.vm.setConfig('client-id', 'client-secret', 'payer-id'); + + expect(store.allConfigs).toStrictEqual({ + null: { + 'SwagPayPal.settings.clientId': 'client-id', + 'SwagPayPal.settings.clientSecret': 'client-secret', + 'SwagPayPal.settings.merchantPayerId': 'payer-id', + 'SwagPayPal.settings.sandbox': false, + }, + }); + + store.setConfig(null, {}); + wrapper.setProps({ type: 'sandbox' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.suffix).toBe('Sandbox'); + wrapper.vm.setConfig('client-id', 'client-secret', 'payer-id'); + + expect(store.allConfigs).toStrictEqual({ + null: { + 'SwagPayPal.settings.clientIdSandbox': 'client-id', + 'SwagPayPal.settings.clientSecretSandbox': 'client-secret', + 'SwagPayPal.settings.merchantPayerIdSandbox': 'payer-id', + 'SwagPayPal.settings.sandbox': true, + }, + }); + }); + + it('should have right onboarding url', async () => { + const wrapper = await createWrapper(); + + const sharedParams = [ + ['channelId', 'partner'], + ['product', 'ppcp'], + ['secondaryProducts', 'advanced_vaulting,PAYMENT_METHODS'], + ['capabilities', 'APPLE_PAY,GOOGLE_PAY,PAY_UPON_INVOICE,PAYPAL_WALLET_VAULTING_ADVANCED'], + ['integrationType', 'FO'], + ['features', 'PAYMENT,REFUND,READ_SELLER_DISPUTE,UPDATE_SELLER_DISPUTE,ADVANCED_TRANSACTIONS_SEARCH,ACCESS_MERCHANT_INFORMATION,TRACKING_SHIPMENT_READWRITE,VAULT,BILLING_AGREEMENT'], + ['displayMode', 'minibrowser'], + ['partnerLogoUrl', 'https://assets.shopware.com/media/logos/shopware_logo_blue.svg'], + ]; + + wrapper.vm.live.sellerNonce = 'live-nonce'; + wrapper.vm.sandbox.sellerNonce = 'sandbox-nonce'; + + wrapper.setProps({ type: 'live' }); + await wrapper.vm.$nextTick(); + + const liveUrl = new URL(wrapper.vm.onboardingUrl); + expect(liveUrl.origin).toBe('https://www.paypal.com'); + expect(liveUrl.pathname).toBe('/bizsignup/partner/entry'); + expect(Array.from(liveUrl.searchParams)).toStrictEqual([ + ...sharedParams, + ['partnerId', 'DYKPBPEAW5JNA'], + ['partnerClientId', 'AR1aQ13lHxH1c6b3CDd8wSY6SWad2Lt5fv5WkNIZg-qChBoGNfHr2kT180otUmvE_xXtwkgahXUBBurW'], + ['sellerNonce', 'live-nonce'], + ['returnToPartnerUrl', 'http://localhost/#/sw/path?ppOnboarding=live'], + ]); + + wrapper.setProps({ type: 'sandbox' }); + await wrapper.vm.$nextTick(); + + const sandboxUrl = new URL(wrapper.vm.onboardingUrl); + expect(sandboxUrl.origin).toBe('https://www.sandbox.paypal.com'); + expect(sandboxUrl.pathname).toBe('/bizsignup/partner/entry'); + expect(Array.from(sandboxUrl.searchParams)).toStrictEqual([ + ...sharedParams, + ['partnerId', '45KXQA7PULGAG'], + ['partnerClientId', 'AQ9g8qMYHpE8s028VCq_GO3Roy9pjeqGDjKTkR_sxzX0FtncBb3QUWbFtoQMtdpe2lG9NpnDT419dK8s'], + ['sellerNonce', 'sandbox-nonce'], + ['returnToPartnerUrl', 'http://localhost/#/sw/path?ppOnboarding=sandbox'], + ]); + }); + + it('should complete onboarding', async () => { + const wrapper = await createWrapper(); + + const store = Shopware.Store.get('swagPayPalSettings'); + + store.setConfig(null, {}); + wrapper.vm.$router.replace({ + query: { + ppOnboarding: 'sandbox', + merchantIdInPayPal: 'payer-id-sandbox', + }, + }); + + wrapper.vm.onMounted(); + + expect(wrapper.vm.$route.query).toStrictEqual({}); + expect(store.allConfigs).toStrictEqual({ + null: { + 'SwagPayPal.settings.merchantPayerIdSandbox': 'payer-id-sandbox', + }, + }); + + store.setConfig(null, {}); + wrapper.vm.$router.replace({ + query: { + ppOnboarding: 'live', + merchantIdInPayPal: 'payer-id-live', + }, + }); + + wrapper.vm.onMounted(); + + expect(wrapper.vm.$route.query).toStrictEqual({}); + expect(store.allConfigs).toStrictEqual({ + null: { + 'SwagPayPal.settings.merchantPayerId': 'payer-id-live', + }, + }); + + store.setConfig(null, {}); + wrapper.vm.$router.replace({ + query: { + ppOnboarding: 'sdf-live', + merchantIdInPayPal: 'merchant-id-live', + }, + }); + + wrapper.vm.onMounted(); + + expect(wrapper.vm.$route.query).toStrictEqual({}); + expect(store.allConfigs).toStrictEqual({ null: {} }); + }); + + it('should be able to change type after creation', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.suffix).toBe(''); + + wrapper.vm.type = 'sandbox'; + + expect(wrapper.vm.suffix).toBe('Sandbox'); + + wrapper.vm.type = 'live'; + + expect(wrapper.vm.suffix).toBe(''); + + wrapper.setProps({ type: 'sandbox' }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.suffix).toBe('Sandbox'); + }); +}); diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-setting/index.ts b/src/Resources/app/administration/src/app/component/swag-paypal-setting/index.ts new file mode 100644 index 000000000..090a58315 --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-setting/index.ts @@ -0,0 +1,151 @@ +import type * as PayPal from 'src/types'; +import template from './swag-paypal-setting.html.twig'; +import './swag-paypal-setting.scss'; +import { SystemConfigDefinition } from '../../../types/system-config'; +import { type SYSTEM_CONFIG, SYSTEM_CONFIGS } from '../../../constant/swag-paypal-settings.constant'; + +const { string, object } = Shopware.Utils; + +/** + * @private - The component has a stable public API (props), but expect that implementation details may change. + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: ['acl'], + + emits: ['update:value'], + + props: { + path: { + required: true, + type: String as PropType, + validation: (value: SYSTEM_CONFIG) => { + return SYSTEM_CONFIGS.includes(value); + }, + }, + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + value() { + return this.settingsStore.getActual(this.path); + }, + + inheritedValue() { + return this.settingsStore.salesChannel + ? this.settingsStore.getRoot(this.path) + : undefined; + }, + + hasParent() { + return !!this.settingsStore.salesChannel; + }, + + pathDomainless() { + return this.path.replace('SwagPayPal.settings.', ''); + }, + + disabled(): boolean { + return !this.acl.can('swag_paypal.editor') + || this.settingsStore.isLoading + || !!this.formAttrs.disabled; + }, + + type() { + return SystemConfigDefinition[this.path]; + }, + + customInheritationCheckFunction() { + return (value: unknown) => value === null || value === undefined; + }, + + label() { + return this.tif(`swag-paypal-setting.label.${this.pathDomainless}`); + }, + + helpText() { + return this.tif( + `swag-paypal-setting.helpText.${this.pathDomainless}.${this.settingsStore.getActual(this.path)}`, + `swag-paypal-setting.helpText.${this.pathDomainless}`, + ); + }, + + hintText() { + return this.tif( + `swag-paypal-setting.hintText.${this.pathDomainless}.${this.settingsStore.getActual(this.path)}`, + `swag-paypal-setting.hintText.${this.pathDomainless}`, + ); + }, + + attrs() { + // normalize attribute keys to camelCase + const entries = Object.entries(this.$attrs).map(([key, value]) => [string.camelCase(key), value]); + const attrs = Object.fromEntries(entries) as Record; + + if (!attrs.hasOwnProperty('label') && this.label) { + attrs.label = this.label; + } + + if (!attrs.hasOwnProperty('helpText') && this.helpText) { + attrs.helpText = this.helpText; + } + + if (!attrs.hasOwnProperty('hintText') && this.hintText) { + attrs.hintText = this.hintText; + } + + return attrs; + }, + + wrapperAttrs(): Record { + return object.pick(this.attrs, [ + 'disabled', + 'helpText', + 'label', + 'required', + ]); + }, + + formAttrs(): Record { + return object.pick(this.attrs, [ + 'disabled', + 'error', + 'labelProperty', + 'options', + 'valueProperty', + ]); + }, + + wrapperClasses(): Record { + return { + 'is--bordered': this.type === 'boolean' && (this.attrs.bordered ?? true), + [`is--${this.type}`]: true, + }; + }, + }, + + methods: { + /** + * Translate if found, otherwise return null + */ + tif(...keys: string[]): string | null { + // $te will also report partial matches as found + const key = keys.find((k) => this.$te(k) && this.$t(k) !== k); + + return key ? this.$t(key) : null; + }, + + setValue(value: PayPal.SystemConfig[keyof PayPal.SystemConfig]) { + if (value !== this.value) { + this.settingsStore.set(this.path, value); + this.$emit('update:value', value ?? undefined); + } + }, + }, +}); diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.html.twig b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.html.twig new file mode 100644 index 000000000..cea1a96da --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.html.twig @@ -0,0 +1,56 @@ + + + diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.scss b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.scss new file mode 100644 index 000000000..a3da0a89c --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.scss @@ -0,0 +1,46 @@ +@import "~scss/variables"; + +.swag-paypal-setting { + &.is--boolean { + display: flex; + flex-direction: row-reverse; + align-items: center; + gap: 8px; + margin-bottom: 32px; + + .sw-inherit-wrapper__toggle-wrapper { + margin-bottom: 0; + width: 100%; + gap: 8px; + } + + &.is--bordered { + border-radius: 4px; + border: 1px solid $color-gray-300; + padding: 16px 16px; + } + + .sw-field--switch { + margin: 0; + } + } + + &.sw-card__title .sw-inherit-wrapper__toggle-wrapper { + font-size: inherit; + } + + // sw-card margin fixes + &:first-child { + .sw-field { + margin-top: 0; + } + } + + &:nth-last-child(1 of :not(.sw-loader)) { + margin-bottom: 0; + + .sw-field { + margin-bottom: 0; + } + } +} diff --git a/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.spec.ts b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.spec.ts new file mode 100644 index 000000000..c809c0b4b --- /dev/null +++ b/src/Resources/app/administration/src/app/component/swag-paypal-setting/swag-paypal-setting.spec.ts @@ -0,0 +1,404 @@ +import { mount } from '@vue/test-utils'; +import SettingsFixture from '../../store/settings.fixture'; +import { INTENTS } from '../../../constant/swag-paypal-settings.constant'; +import SwagPayPalSetting from '.'; + +Shopware.Component.register('swag-paypal-setting', Promise.resolve(SwagPayPalSetting)); + +async function createWrapper(props: $TSFixMe = { path: 'SwagPayPal.settings.clientId' }, translations: Record = {}) { + return mount( + await Shopware.Component.build('swag-paypal-setting') as typeof SwagPayPalSetting, + { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + props, + global: { + mocks: { $t: (key: string) => translations[key] ?? key }, + provide: { acl: { can: () => true } }, + stubs: { + 'sw-inherit-wrapper': await wrapTestComponent('sw-inherit-wrapper', { sync: true }), + 'sw-inheritance-switch': await wrapTestComponent('sw-inheritance-switch', { sync: true }), + 'sw-help-text': await wrapTestComponent('sw-help-text', { sync: true }), + 'sw-icon': await wrapTestComponent('sw-icon', { sync: true }), + 'sw-icon-deprecated': await wrapTestComponent('sw-icon-deprecated', { sync: true }), + // type === boolean + 'sw-switch-field': await wrapTestComponent('sw-switch-field', { sync: true }), + 'sw-switch-field-deprecated': await wrapTestComponent('sw-switch-field-deprecated', { sync: true }), + 'sw-checkbox-field': await wrapTestComponent('sw-checkbox-field', { sync: true }), + 'sw-checkbox-field-deprecated': await wrapTestComponent('sw-checkbox-field-deprecated', { sync: true }), + // type === string + 'sw-text-field': await wrapTestComponent('sw-text-field', { sync: true }), + 'sw-text-field-deprecated': await wrapTestComponent('sw-text-field-deprecated', { sync: true }), + // type === string + options + 'sw-single-select': await wrapTestComponent('sw-single-select', { sync: true }), + 'sw-select-base': await wrapTestComponent('sw-select-base', { sync: true }), + // field bases + 'sw-contextual-field': await wrapTestComponent('sw-contextual-field', { sync: true }), + 'sw-block-field': await wrapTestComponent('sw-block-field', { sync: true }), + 'sw-base-field': await wrapTestComponent('sw-base-field', { sync: true }), + }, + }, + }, + ); +} + +describe('swag-paypal-setting', () => { + const store = Shopware.Store.get('swagPayPalSettings'); + + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should have computed label, helpText and hintText', async () => { + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.clientId' }, + { + 'swag-paypal-setting.label.clientId': 'Label text', + 'swag-paypal-setting.helpText.clientId': 'Help text', + 'swag-paypal-setting.hintText.clientId': 'Hint text', + }, + ); + + expect(wrapper.vm.label).toBe('Label text'); + expect(wrapper.vm.helpText).toBe('Help text'); + expect(wrapper.vm.hintText).toBe('Hint text'); + expect(wrapper.vm.attrs.label).toBe(wrapper.vm.label); + expect(wrapper.vm.attrs.helpText).toBe(wrapper.vm.helpText); + expect(wrapper.vm.attrs.hintText).toBe(wrapper.vm.hintText); + }); + + it('should have not overridden label, helpText and hintText', async () => { + const wrapper = await createWrapper( + { + path: 'SwagPayPal.settings.clientId', + label: 'Overridden label', + helpText: undefined, + hintText: 'Overridden hint text', + }, + { + 'swag-paypal-setting.label.clientId': 'Label text', + 'swag-paypal-setting.helpText.clientId': 'Help text', + 'swag-paypal-setting.hintText.clientId': 'Hint text', + }, + ); + + expect(wrapper.vm.label).toBe('Label text'); + expect(wrapper.vm.helpText).toBe('Help text'); + expect(wrapper.vm.hintText).toBe('Hint text'); + expect(wrapper.vm.attrs.label).toBe('Overridden label'); + expect(wrapper.vm.attrs.helpText).toBeUndefined(); + expect(wrapper.vm.attrs.hintText).toBe('Overridden hint text'); + }); + + it('should have normalized attrs', async () => { + const wrapper = await createWrapper({ + path: 'SwagPayPal.settings.clientId', + 'not-camel-case': 'value', + 'help-text': 'Help text', + }); + + expect(wrapper.vm.attrs).toStrictEqual({ + notCamelCase: 'value', + helpText: 'Help text', + }); + }); + + it('should have attrs', async () => { + const attrs = { + bordered: true, + disabled: true, + error: { code: 'TEST', detail: 'Test error' }, + helpText: 'Help text', + label: 'Label text', + labelProperty: 'label', + options: [], + required: true, + valueProperty: 'value', + }; + + const wrapper = await createWrapper({ path: 'SwagPayPal.settings.clientId', ...attrs }); + + expect(wrapper.vm.type).toBe('string'); + expect(wrapper.vm.attrs).toStrictEqual(attrs); + expect(wrapper.vm.wrapperAttrs).toStrictEqual({ + disabled: true, + helpText: 'Help text', + label: 'Label text', + required: true, + }); + expect(wrapper.vm.formAttrs).toStrictEqual({ + disabled: true, + error: { code: 'TEST', detail: 'Test error' }, + labelProperty: 'label', + options: [], + valueProperty: 'value', + }); + }); + + it('should find translations', async () => { + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.clientId' }, + { transOne: 'Label text', transTwo: 'Help text' }, + ); + + expect(wrapper.vm.tif('non-existing')).toBeNull(); + expect(wrapper.vm.tif('transOne')).toBe('Label text'); + expect(wrapper.vm.tif('transOne', 'transTwo')).toBe('Label text'); + expect(wrapper.vm.tif('non-existing', 'transTwo')).toBe('Help text'); + }); + + it('should be a text field without inheritance', async () => { + store.setConfig(null, SettingsFixture.WithCredentials); + + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.clientId' }, + { 'swag-paypal-setting.label.clientId': 'Client ID' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe('some-client-id'); + expect(wrapper.vm.inheritedValue).toBeUndefined(); + expect(wrapper.vm.hasParent).toBe(false); + expect(wrapper.vm.pathDomainless).toBe('clientId'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('string'); + expect(wrapper.vm.label).toBe('Client ID'); + expect(wrapper.vm.helpText).toBeNull(); + expect(wrapper.vm.hintText).toBeNull(); + + const field = wrapper.findComponent('.sw-field--text'); + + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe('some-client-id'); + expect(field.vm.$attrs.name).toBe('SwagPayPal.settings.clientId'); + expect(field.vm.$attrs.disabled).toBe(false); + expect(field.vm.$attrs['map-inheritance']).toBeUndefined(); + + Object.keys(wrapper.vm.formAttrs).forEach((key) => { + expect(field.vm.$attrs).toHaveProperty(key); + expect(field.vm.$attrs[key]).toBe(wrapper.vm.formAttrs[key]); + }); + }); + + it('should be a boolean field without inheritance', async () => { + store.setConfig(null, SettingsFixture.WithCredentials); + + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.sandbox' }, + { 'swag-paypal-setting.label.sandbox': 'Sandbox' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe(false); + expect(wrapper.vm.inheritedValue).toBeUndefined(); + expect(wrapper.vm.hasParent).toBe(false); + expect(wrapper.vm.pathDomainless).toBe('sandbox'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('boolean'); + expect(wrapper.vm.label).toBe('Sandbox'); + expect(wrapper.vm.helpText).toBeNull(); + expect(wrapper.vm.hintText).toBeNull(); + + const field = wrapper.findComponent('.sw-field--switch'); + + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe(false); + expect(field.vm.$attrs.name).toBe('SwagPayPal.settings.sandbox'); + expect(field.vm.$attrs.disabled).toBe(false); + + Object.keys(wrapper.vm.formAttrs).forEach((key) => { + expect(field.vm.$attrs).toHaveProperty(key); + expect(field.vm.$attrs[key]).toBe(wrapper.vm.formAttrs[key]); + }); + }); + + it('should be a select field without inheritance', async () => { + store.setConfig(null, SettingsFixture.WithCredentials); + const options = INTENTS.map((intent) => ({ value: intent, label: intent })); + + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.intent', options }, + { 'swag-paypal-setting.label.intent': 'Intent' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe('CAPTURE'); + expect(wrapper.vm.inheritedValue).toBeUndefined(); + expect(wrapper.vm.hasParent).toBe(false); + expect(wrapper.vm.pathDomainless).toBe('intent'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('string'); + expect(wrapper.vm.label).toBe('Intent'); + expect(wrapper.vm.helpText).toBeNull(); + expect(wrapper.vm.hintText).toBeNull(); + + const field = wrapper.findComponent('.sw-single-select'); + + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe('CAPTURE'); + expect(field.vm.$attrs.name).toBe('SwagPayPal.settings.intent'); + expect(field.vm.$attrs.disabled).toBe(false); + + Object.keys(wrapper.vm.formAttrs).forEach((key: string) => { + if (key === 'options') { + expect(field.vm).toHaveProperty(key); + expect(field.vm[key]).toStrictEqual(wrapper.vm.formAttrs[key]); + } else { + expect(field.vm.$attrs).toHaveProperty(key); + expect(field.vm.$attrs[key]).toBe(wrapper.vm.formAttrs[key]); + } + }); + }); + + it('should be a text field with inheritance', async () => { + store.setConfig(null, SettingsFixture.WithCredentials); + store.setConfig('some-sales-channel', SettingsFixture.All); + store.salesChannel = 'some-sales-channel'; + + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.clientId' }, + { 'swag-paypal-setting.label.clientId': 'Client ID' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe(''); + expect(wrapper.vm.inheritedValue).toBe('some-client-id'); + expect(wrapper.vm.hasParent).toBe(true); + expect(wrapper.vm.pathDomainless).toBe('clientId'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('string'); + expect(wrapper.vm.wrapperAttrs.label).toBe('Client ID'); + + // field shows actual value + const field = wrapper.findComponent('.sw-field--text'); + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe(''); + expect(field.vm.$attrs.disabled).toBe(false); + + // inheritance switch exists and is not inherited + const inheritSwitch = wrapper.findComponent('.sw-inheritance-switch'); + expect(inheritSwitch.exists()).toBe(true); + expect(inheritSwitch.vm.isInherited).toBe(false); + + // Switch inheritance - value should be restored + const icon = inheritSwitch.findComponent('.sw-icon'); + expect(icon.exists()).toBe(true); + await icon.trigger('click'); + + expect(inheritSwitch.vm.isInherited).toBe(true); + expect(wrapper.vm.value).toBeUndefined(); + expect(wrapper.vm.inheritedValue).toBe('some-client-id'); + expect(field.vm.$attrs.disabled).toBe(true); + expect(field.vm.value).toBe('some-client-id'); + + // Switch to "All Sales Channels" - inheritance should be disabled + store.salesChannel = null; + + await wrapper.vm.$nextTick(); + + expect(inheritSwitch.exists()).toBe(false); + }); + + it('should be a select field with inheritance', async () => { + store.setConfig(null, SettingsFixture.Default); + store.setConfig('some-sales-channel', { + 'SwagPayPal.settings.intent': 'AUTHORIZE', + }); + store.salesChannel = 'some-sales-channel'; + + const wrapper = await createWrapper( + { + path: 'SwagPayPal.settings.intent', + options: INTENTS.map((intent) => ({ value: intent, label: intent })), + }, + { 'swag-paypal-setting.label.intent': 'Intent' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe('AUTHORIZE'); + expect(wrapper.vm.inheritedValue).toBe('CAPTURE'); + expect(wrapper.vm.hasParent).toBe(true); + expect(wrapper.vm.pathDomainless).toBe('intent'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('string'); + expect(wrapper.vm.wrapperAttrs.label).toBe('Intent'); + + // field shows actual value + const field = wrapper.findComponent('.sw-single-select'); + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe('AUTHORIZE'); + expect(field.vm.$attrs.disabled).toBe(false); + + // inheritance switch exists and is not inherited + const inheritSwitch = wrapper.findComponent('.sw-inheritance-switch'); + expect(inheritSwitch.exists()).toBe(true); + expect(inheritSwitch.vm.isInherited).toBe(false); + + // Switch inheritance - value should be restored + const icon = inheritSwitch.findComponent('.sw-icon'); + expect(icon.exists()).toBe(true); + await icon.trigger('click'); + + expect(inheritSwitch.vm.isInherited).toBe(true); + expect(wrapper.vm.value).toBeUndefined(); + expect(wrapper.vm.inheritedValue).toBe('CAPTURE'); + expect(field.vm.$attrs.disabled).toBe(true); + expect(field.vm.value).toBe('CAPTURE'); + + // Switch to "All Sales Channels" - inheritance should be disabled + store.salesChannel = null; + + await wrapper.vm.$nextTick(); + + expect(inheritSwitch.exists()).toBe(false); + }); + + it('should be a boolean field with inheritance', async () => { + store.setConfig(null, SettingsFixture.Default); + store.setConfig('some-sales-channel', { 'SwagPayPal.settings.sandbox': true }); + store.salesChannel = 'some-sales-channel'; + + const wrapper = await createWrapper( + { path: 'SwagPayPal.settings.sandbox' }, + { 'swag-paypal-setting.label.sandbox': 'Sandbox' }, + ); + + // computed properties + expect(wrapper.vm.value).toBe(true); + expect(wrapper.vm.inheritedValue).toBe(false); + expect(wrapper.vm.hasParent).toBe(true); + expect(wrapper.vm.pathDomainless).toBe('sandbox'); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.type).toBe('boolean'); + + // field shows actual value + const field = wrapper.findComponent('.sw-field--switch'); + expect(field.exists()).toBe(true); + expect(field.vm.value).toBe(true); + expect(field.vm.$attrs.disabled).toBe(false); + + // inheritance switch exists and is not inherited + const inheritSwitch = wrapper.findComponent('.sw-inheritance-switch'); + expect(inheritSwitch.exists()).toBe(true); + expect(inheritSwitch.vm.isInherited).toBe(false); + + // Switch inheritance - value should be restored + const icon = inheritSwitch.findComponent('.sw-icon'); + expect(icon.exists()).toBe(true); + await icon.trigger('click'); + + expect(inheritSwitch.vm.isInherited).toBe(true); + expect(wrapper.vm.value).toBeUndefined(); + expect(wrapper.vm.inheritedValue).toBe(false); + expect(field.vm.$attrs.disabled).toBe(true); + expect(field.vm.value).toBe(false); + + // Switch to "All Sales Channels" - inheritance should be disabled + store.salesChannel = null; + + await wrapper.vm.$nextTick(); + + expect(inheritSwitch.exists()).toBe(false); + }); +}); diff --git a/src/Resources/app/administration/src/app/index.ts b/src/Resources/app/administration/src/app/index.ts new file mode 100644 index 000000000..4c7c83e1b --- /dev/null +++ b/src/Resources/app/administration/src/app/index.ts @@ -0,0 +1,17 @@ +import './acl'; +import './store/swag-paypal-merchant-information.store'; +import './store/swag-paypal-settings.store'; + +Shopware.Component.register('swag-paypal-onboarding-button', () => import('./component/swag-paypal-onboarding-button')); +Shopware.Component.register('swag-paypal-setting', () => import('./component/swag-paypal-setting')); + +// synchronise salesChannel of stores +Shopware.Vue.watch( + () => Shopware.Store.get('swagPayPalSettings').salesChannel, + (salesChannel) => { Shopware.Store.get('swagPayPalMerchantInformation').salesChannel = salesChannel; }, +); + +Shopware.Vue.watch( + () => Shopware.Store.get('swagPayPalMerchantInformation').salesChannel, + (salesChannel) => { Shopware.Store.get('swagPayPalSettings').salesChannel = salesChannel; }, +); diff --git a/src/Resources/app/administration/src/app/snippet/de-DE.json b/src/Resources/app/administration/src/app/snippet/de-DE.json new file mode 100644 index 000000000..041035d71 --- /dev/null +++ b/src/Resources/app/administration/src/app/snippet/de-DE.json @@ -0,0 +1,118 @@ +{ + "swag-paypal": { + "errors": { + "UNKNOWN": "Ein unbekannter Fehler ist aufgetreten", + "SWAG_PAYPAL__API_INVALID_CREDENTIALS": "Die API-Zugangsdaten sind ungültig", + "SWAG_PAYPAL__API_NOT_AVAILABLE": "Die PayPal-Services sind derzeit nicht verfügbar. Bitte versuchen Sie es später errneut.", + "SWAG_PAYPAL__INVALID_API_CREDENTIALS": "Die API-Zugangsdaten sind ungültig" + }, + "notifications": { + "save": { + "title": "PayPal Einstellungen", + "webhookMessage": "Der Webhook konnte nicht gespeichert werden:
{message}", + "errorMessage": "Speicherung fehlgeschlagen:
{message}" + } + } + }, + "swag-paypal-onboarding-button": { + "sandbox": { + "title": "PayPal-Sandbox-Konto verbinden", + "changeTitle": "Anderes Sandbox-Konto verbinden", + "onboardingTitle": "PayPal Sandbox-Onboarding starten" + }, + "live": { + "title": "PayPal-Konto verbinden", + "changeTitle": "Anderes Konto verbinden", + "onboardingTitle": "PayPal Onboarding starten", + "restartOnboardingTitle": "PayPal Onboarding wiederholen" + } + }, + "swag-paypal-setting": { + "label": { + "clientId": "Client-ID", + "clientSecret": "Client-Secret", + "merchantPayerId": "PayPal-Händler-ID", + "clientIdSandbox": "Sandbox-Client-ID", + "clientSecretSandbox": "Sandbox-Client-Secret", + "merchantPayerIdSandbox": "Sandbox-PayPal-Händler-ID", + "sandbox": "Sandbox aktivieren", + "intent": "Zahlungsabschluss", + "submitCart": "Warenkorb übertragen", + "brandName": "Eigener Markenname auf der PayPal-Seite", + "landingPage": "PayPal-Landingpage", + "sendOrderNumber": "Bestellnummer übertragen", + "orderNumberPrefix": "Bestellnummer-Präfix", + "orderNumberSuffix": "Bestellnummer-Suffix", + "excludedProductIds": "Ausgeschlossene Produkte", + "excludedProductStreamIds": "Ausgeschlossene dynamische Produktgruppen", + "ecsDetailEnabled": "'Direkt zu PayPal' auf Detailseite", + "ecsCartEnabled": "'Direkt zu PayPal' im Warenkorb", + "ecsOffCanvasEnabled": "'Direkt zu PayPal' im Off-Canvas Warenkorb", + "ecsLoginEnabled": "'Direkt zu PayPal' auf Loginseite", + "ecsListingEnabled": "'Direkt zu PayPal' auf Listingseiten", + "ecsButtonColor": "Buttonfarbe", + "ecsButtonShape": "Buttonform", + "ecsSubmitCart": "Warenkorb übertragen", + "ecsButtonLanguageIso": "Buttonsprache", + "ecsShowPayLater": "'Später Bezahlen' neben dem 'PayPal Checkout'-Button anzeigen", + "installmentBannerDetailPageEnabled": "'Später Bezahlen'-Banner auf Detailseite", + "installmentBannerCartEnabled": "'Später Bezahlen'-Banner im Warenkorb", + "installmentBannerOffCanvasCartEnabled": "'Später Bezahlen'-Banner im Off-Canvas-Warenkorb", + "installmentBannerLoginPageEnabled": "'Später Bezahlen'-Banner auf Loginseite", + "installmentBannerFooterEnabled": "'Später Bezahlen'-Banner im Footer", + "acdcForce3DS": "Zahlungen aus Nicht-3DS-Ländern blockieren", + "puiCustomerServiceInstructions": "Kundenservice-Anweisungen für Rechnungskauf", + "spbCheckoutEnabled": "Smart Payment Buttons aktivieren", + "spbButtonLanguageIso": "Buttonsprache", + "spbAlternativePaymentMethodsEnabled": "Aktiviert die alternativen Zahlungsarten der Smart Payment Buttons.", + "spbShowPayLater": "'Später Bezahlen'-Button neben PayPal-Button anzeigen", + "spbButtonColor": "Buttonfarbe", + "spbButtonShape": "Buttonform", + "vaultingEnabledWallet": "Vaulting für PayPal-Zahlungen aktivieren", + "vaultingEnabledACDC": "Vaulting für Kredit- und Debitkarten-Zahlungen verwenden", + "vaultingEnabledVenmo": "Vaulting für Venmo-Zahlungen verwenden", + "crossBorderMessagingEnabled" : "Länderübergreifende Lokalisierung der \"Später bezahlen\"-Nachricht aktivieren", + "crossBorderBuyerCountry" : "Lokalisierung" + }, + "helpText": { + "clientId": "Die Client-ID der REST-API, die das Plugin dazu verwendet, sich mit der PayPal-API zu authentifizieren.", + "clientSecret": "Das Client-Secret der REST-API, das das Plugin dazu verwendet, sich mit der PayPal-API zu authentifizieren.", + "merchantPayerId": "Die PayPal-Händler-ID, die dem PayPal-Konto zugeordnet ist (siehe Geschäftsangaben in den PayPal Kontoeinstellungen).", + "clientIdSandbox": "Die Client-ID der REST-API, die das Plugin im Testfall dazu verwendet, sich mit der PayPal-API zu authentifizieren.", + "clientSecretSandbox": "Das Client-Secret der REST-API, das das Plugin im Testfall dazu verwendet, sich mit der PayPal-API zu authentifizieren.", + "merchantPayerIdSandbox": "Die PayPal-Händler-ID, die dem Sandbox-PayPal-Konto zugeordnet ist (siehe Geschäftsangaben in den PayPal Kontoeinstellungen).", + "sandbox": "Aktiviere diese Option, um die Integration zu testen.", + "submitCart": "Wenn diese Option aktiv ist, werden beim Checkout die Warenkorbdaten an PayPal übertragen.", + "brandName": "Dieser Text wird als Markenname auf der PayPal-Zahlungsseite angezeigt.", + "sendOrderNumber": "Wenn diese Option aktiv ist, wird beim Checkout die Bestellnummer an PayPal als Rechnungsnummer übertragen.", + "orderNumberPrefix": "Dieser Text wird vor die ursprüngliche Bestellnummer gehängt (z.B. MeinShop_SW20001). Das hilft dabei der Identifizierung des Shops, in dem die Zahlung ausgeführt wurde. Du findest diese als Rechnungsnummer in Deinem PayPal-Dashboard.", + "orderNumberSuffix": "Dieser Text wird an die ursprüngliche Bestellnummer gehängt (z.B. SW20001_MeinShop). Das hilft dabei der Identifizierung des Shops, in dem die Zahlung ausgeführt wurde. Du findest diese als Rechnungsnummer in Deinem PayPal-Dashboard.", + "excludedProductIds": "Hier ausgewählte Produkte können nicht mit PayPal gekauft werden.", + "excludedProductStreamIds": "Hier ausgewählte dynamische Produktgruppen können nicht mit PayPal gekauft werden.", + "ecsDetailEnabled": "Wenn diese Option aktiv ist, wird der Express Checkout Button auf jeder Produktdetailseite angezeigt.", + "ecsCartEnabled": "Wenn diese Option aktiv ist, wird der Express Checkout Button auf der Warenkorbseite angezeigt.", + "ecsOffCanvasEnabled": "Wenn diese Option aktiv ist, wird der Express Checkout Button in dem Off-Canvas-Warenkorb angezeigt.", + "ecsLoginEnabled": "Wenn diese Option aktiv ist, wird der Express Checkout Button auf der Login- und Registrierungsseite angezeigt.", + "ecsListingEnabled": "Wenn diese Option aktiv ist, wird der Express Checkout Button auf Listingseiten angezeigt.", + "ecsSubmitCart": "Wenn diese Option aktiv ist, wird der Warenkorb bei Express-Bestellungen an PayPal übertragen.", + "ecsButtonLanguageIso": "Wenn nicht gesetzt, wird die Sprache des Verkaufskanals verwendet.", + "ecsShowPayLater": "Die Schaltfläche 'Später Bezahlen' wird auf denselben Seiten und im selben Design wie die Schaltfläche 'Direkt zu PayPal' angezeigt.", + "installmentBannerDetailPageEnabled": "Wenn diese Option aktiv ist, wird der 'Später Bezahlen'-Banner auf jeder Produktdetailseite angezeigt.", + "installmentBannerCartEnabled": "Wenn diese Option aktiv ist, wird der 'Später Bezahlen'-Banner im Warenkorb angezeigt.", + "installmentBannerOffCanvasCartEnabled": "Wenn diese Option aktiv ist, wird der 'Später Bezahlen'-Banner im Off-Canvas-Warenkorb angezeigt.", + "installmentBannerLoginPageEnabled": "Wenn diese Option aktiv ist, wird der 'Später Bezahlen'-Banner auf der Login- und Registrierungsseite angezeigt.", + "installmentBannerFooterEnabled": "Wenn diese Option aktiv ist, wird der 'Später Bezahlen'-Banner im Footer angezeigt.", + "acdcForce3DS": "PayPal prüft auf Basis der präsentierten Kredit- oder Debitkarte, ob 3DS (Starke Kunden-Authentifizierung) erforderlich ist. Durch das Setzen dieser Option werden Zahlungsversuche ohne 3DS-Check abgelehnt.", + "puiCustomerServiceInstructions": "Diese Anweisungen werden an PayPal & RatePay übermittelt und in Kunden-E-Mails verwendet.", + "spbButtonLanguageIso": "Wenn nicht gesetzt, wird die Sprache des Verkaufskanals verwendet.", + "spbAlternativePaymentMethodsEnabled": "Die alternativen Zahlungsarten sind Kredit- und Debitkarten und weitere." + }, + "hintText": { + "landingPage": { + "LOGIN": "Anmeldung: Auf der PayPal-Seite wird der Login als Landingpage angezeigt.", + "GUEST_CHECKOUT": "Gast-Checkout: Auf der PayPal-Seite werden direkt Zahlungsdaten des Kunden abgefragt, ohne dass dieser sich bei PayPal einloggen muss.", + "NO_PREFERENCE": "Keine Präferenz: PayPal entscheidet auf Grundlage früherer Interaktionen des Kunden mit PayPal welche Seite angezeigt wird." + } + } + } +} diff --git a/src/Resources/app/administration/src/app/snippet/en-GB.json b/src/Resources/app/administration/src/app/snippet/en-GB.json new file mode 100644 index 000000000..c924f2c7b --- /dev/null +++ b/src/Resources/app/administration/src/app/snippet/en-GB.json @@ -0,0 +1,118 @@ +{ + "swag-paypal": { + "errors": { + "UNKNOWN": "An unknown error occurred", + "SWAG_PAYPAL__API_INVALID_CREDENTIALS": "The API credentials are invalid", + "SWAG_PAYPAL__API_NOT_AVAILABLE": "PayPal services are currently unavailable. Please try again later.", + "SWAG_PAYPAL__INVALID_API_CREDENTIALS": "The API credentials are invalid" + }, + "notifications": { + "save": { + "title": "PayPal Settings", + "webhookMessage": "The webhook could not be saved:
{message}", + "errorMessage": "Saving failed:
{message}" + } + } + }, + "swag-paypal-onboarding-button": { + "sandbox": { + "title": "Connect sandbox account", + "changeTitle": "Connect different sandbox account", + "onboardingTitle": "Start PayPal sandbox onboarding" + }, + "live": { + "title": "Connect PayPal account", + "changeTitle": "Connect different account", + "onboardingTitle": "Start PayPal onboarding", + "restartOnboardingTitle": "Restart PayPal onboarding" + } + }, + "swag-paypal-setting": { + "label": { + "clientId": "Client ID", + "clientSecret": "Client secret", + "merchantPayerId": "PayPal Merchant ID", + "clientIdSandbox": "Sandbox client ID", + "clientSecretSandbox": "Sandbox client secret", + "merchantPayerIdSandbox": "Sandbox PayPal Merchant ID", + "sandbox": "Enable sandbox", + "intent": "Payment acquisition", + "submitCart": "Submit cart", + "brandName": "Your own brand name on PayPal page", + "landingPage": "PayPal landing page", + "sendOrderNumber": "Submit order number", + "orderNumberPrefix": "Order number prefix", + "orderNumberSuffix": "Order number suffix", + "excludedProductIds": "Excluded products", + "excludedProductStreamIds": "Excluded dynamic product groups", + "ecsDetailEnabled": "'PayPal Checkout' on detail page", + "ecsCartEnabled": "'PayPal Checkout' on cart", + "ecsOffCanvasEnabled": "'PayPal Checkout' on off-canvas cart", + "ecsLoginEnabled": "'PayPal Checkout' on login page", + "ecsListingEnabled": "'PayPal Checkout' on listing pages", + "ecsButtonColor": "Button color", + "ecsButtonShape": "Button shape", + "ecsSubmitCart": "Submit cart", + "ecsButtonLanguageIso": "Button locale", + "ecsShowPayLater": "Display 'Pay Later' button next to the 'PayPal Checkout' button", + "installmentBannerDetailPageEnabled": "'Pay Later' banner on detail page", + "installmentBannerCartEnabled": "'Pay Later' banner on cart", + "installmentBannerOffCanvasCartEnabled": "'Pay Later' banner on off-canvas cart", + "installmentBannerLoginPageEnabled": "'Pay Later' banner on login page", + "installmentBannerFooterEnabled": "'Pay Later' banner on footer", + "acdcForce3DS": "Block payments from non-3DS countries", + "puiCustomerServiceInstructions": "Customer service instructions for Pay upon invoice", + "spbCheckoutEnabled": "Enable Smart Payment Buttons", + "spbButtonLanguageIso": "Button locale", + "spbAlternativePaymentMethodsEnabled": "Enable the alternative payment methods for the Smart Payment Buttons.", + "spbShowPayLater": "Display 'Pay Later' button next to PayPal button", + "spbButtonColor": "Button color", + "spbButtonShape": "Button shape", + "vaultingEnabledWallet": "Enable Vaulting for PayPal payments", + "vaultingEnabledACDC": "Enable Vaulting for credit and debit cards", + "vaultingEnabledVenmo": "Enable Vaulting for Venmo payments", + "crossBorderMessagingEnabled" : "Enable cross-border localization of Pay Later message", + "crossBorderBuyerCountry" : "Localization" + }, + "helpText": { + "clientId": "The REST API client ID is used to authenticate this plugin with the PayPal API.", + "clientSecret": "The REST API client secret is used to authenticate this plugin with the PayPal API.", + "merchantPayerId": "The PayPal Merchant ID assigned to your PayPal account (see Business Information in PayPal account settings).", + "clientIdSandbox": "The REST API client ID is used while testing to authenticate this plugin with the PayPal API.", + "clientSecretSandbox": "The REST API client secret is used while testing to authenticate this plugin with the PayPal API.", + "merchantPayerIdSandbox": "The PayPal Merchant ID assigned to your PayPal sandbox account (see Business Information in PayPal account settings).", + "sandbox": "Enable, if you want to test the PayPal integration.", + "submitCart": "If this option is active, cart data will be submitted to PayPal at checkout.", + "brandName": "This text will be displayed as the brand name on the PayPal payment page.", + "sendOrderNumber": "If this option is active, the order number will be submitted to PayPal as invoice ID at checkout.", + "orderNumberPrefix": "This text is placed before the original order number (e.g MyShop_SW20001). This helps to identify the shop where the payment was made. You can find it as invoice ID in your PayPal dashboard.", + "orderNumberSuffix": "This text is placed after the original order number (e.g SW20001_MyShop). This helps to identify the shop where the payment was made. You can find it as invoice ID in your PayPal dashboard.", + "excludedProductIds": "Products selected here cannot be purchased with PayPal.", + "excludedProductStreamIds": "Products included in the dynamic product groups selected here cannot be purchased with PayPal.", + "ecsDetailEnabled": "If this option is active, the Express Checkout button will be shown on each product detail page.", + "ecsCartEnabled": "If this option is active, the Express Checkout button will be shown on the cart.", + "ecsOffCanvasEnabled": "If this option is active, the Express Checkout button will be shown on the off-canvas cart.", + "ecsLoginEnabled": "If this option is active, the Express Checkout button will be shown on the login and register page.", + "ecsListingEnabled": "If this option is active, the Express Checkout button will be shown on listing pages.", + "ecsSubmitCart": "If this option is active, the cart will be submitted to PayPal for Express orders.", + "ecsButtonLanguageIso": "If not set, the sales channel language will be used.", + "ecsShowPayLater": "The 'Pay Later' button will be displayed on the same pages and in the same design as the 'PayPal Checkout' button.", + "installmentBannerDetailPageEnabled": "If this option is active, the 'Pay Later' banner will be shown on each product detail page.", + "installmentBannerCartEnabled": "If this option is active, the 'Pay Later' banner will be shown on the cart.", + "installmentBannerOffCanvasCartEnabled": "If this option is active, the 'Pay Later' banner will be shown on the off-canvas cart.", + "installmentBannerLoginPageEnabled": "If this option is active, the 'Pay Later' banner will be shown on the login and register page.", + "installmentBannerFooterEnabled": "If this option is active, the 'Pay Later' banner will be shown on the footer.", + "acdcForce3DS": "PayPal checks whether 3DS (Strong Customer Authentication) is required based on the credit or debit card presented. Setting this option will reject payment attempts without a 3DS check.", + "puiCustomerServiceInstructions": "These instructions will be submitted to PayPal & RatePay and shown in emails for the customer.", + "spbButtonLanguageIso": "If not set, the sales channel language will be used.", + "spbAlternativePaymentMethodsEnabled": "Alternative payment methods are credit- and debit cards and more." + }, + "hintText": { + "landingPage": { + "LOGIN": "Login: The PayPal site displays a login screen as landing page.", + "GUEST_CHECKOUT": "Guest Checkout: The PayPal site displays a form for payment information, the customer does not need to log in.", + "NO_PREFERENCE": "No preference: PayPal decides which page is shown, depending on the previous interaction of the customer with PayPal." + } + } + } +} diff --git a/src/Resources/app/administration/src/app/store/merchant-information.fixture.ts b/src/Resources/app/administration/src/app/store/merchant-information.fixture.ts new file mode 100644 index 000000000..ebf9c8ea6 --- /dev/null +++ b/src/Resources/app/administration/src/app/store/merchant-information.fixture.ts @@ -0,0 +1,157 @@ +import type * as PayPal from 'src/types'; + +const Default = { + merchantIntegrations: { + merchant_id: '2CWUTJMHUSECB', + tracking_id: 'test@example.com', + products: [ + { name: 'BASIC_PPPLUS_CORE' }, + { name: 'BASIC_PPPLUS_PUI' }, + { name: 'BASIC_PPPLUS_GUEST_CC' }, + { name: 'BASIC_PPPLUS_GUEST_ELV' }, + { + name: 'PPCP_STANDARD', + vetting_status: 'SUBSCRIBED', + capabilities: [ + 'ACCEPT_DONATIONS', + 'BANK_MANAGED_WITHDRAWAL', + 'GUEST_CHECKOUT', + 'INSTALLMENTS', + 'PAY_WITH_PAYPAL', + 'PAYPAL_CHECKOUT_ALTERNATIVE_PAYMENT_METHODS', + 'PAYPAL_CHECKOUT_PAY_WITH_PAYPAL_CREDIT', + 'PAYPAL_CHECKOUT', + 'QR_CODE', + 'SEND_INVOICE', + 'SUBSCRIPTIONS', + 'WITHDRAW_FUNDS_TO_DOMESTIC_BANK', + ], + }, + { + name: 'PAYMENT_METHODS', + vetting_status: 'SUBSCRIBED', + capabilities: [ + 'APPLE_PAY', + 'GOOGLE_PAY', + 'IDEAL', + 'PAY_UPON_INVOICE', + 'PAY_WITH_PAYPAL', + 'SEPA', + 'VAT_TAX', + ], + }, + { + name: 'ADVANCED_VAULTING', + vetting_status: 'SUBSCRIBED', + capabilities: ['PAYPAL_WALLET_VAULTING_ADVANCED'], + }, + { + name: 'PPCP_CUSTOM', + vetting_status: 'SUBSCRIBED', + capabilities: [ + 'AMEX_OPTBLUE', + 'APPLE_PAY', + 'CARD_PROCESSING_VIRTUAL_TERMINAL', + 'COMMERCIAL_ENTITY', + 'CUSTOM_CARD_PROCESSING', + 'DEBIT_CARD_SWITCH', + 'FRAUD_TOOL_ACCESS', + 'GOOGLE_PAY', + 'PAY_UPON_INVOICE', + 'PAYPAL_WALLET_VAULTING_ADVANCED', + ], + }, + ], + capabilities: [ + { status: 'ACTIVE', name: 'ACCEPT_DONATIONS' }, + { status: 'ACTIVE', name: 'AMEX_OPTBLUE' }, + { status: 'ACTIVE', name: 'APPLE_PAY' }, + { status: 'ACTIVE', name: 'BANK_MANAGED_WITHDRAWAL' }, + { status: 'ACTIVE', name: 'CARD_PROCESSING_VIRTUAL_TERMINAL' }, + { status: 'ACTIVE', name: 'COMMERCIAL_ENTITY' }, + { status: 'ACTIVE', name: 'CUSTOM_CARD_PROCESSING' }, + { status: 'ACTIVE', name: 'DEBIT_CARD_SWITCH' }, + { status: 'ACTIVE', name: 'FRAUD_TOOL_ACCESS' }, + { status: 'ACTIVE', name: 'GOOGLE_PAY' }, + { status: 'ACTIVE', name: 'GUEST_CHECKOUT' }, + { status: 'ACTIVE', name: 'IDEAL' }, + { status: 'ACTIVE', name: 'INSTALLMENTS' }, + { status: 'ACTIVE', name: 'PAY_UPON_INVOICE' }, + { status: 'ACTIVE', name: 'PAY_WITH_PAYPAL' }, + { status: 'ACTIVE', name: 'PAYPAL_CHECKOUT_ALTERNATIVE_PAYMENT_METHODS' }, + { status: 'ACTIVE', name: 'PAYPAL_CHECKOUT_PAY_WITH_PAYPAL_CREDIT' }, + { status: 'ACTIVE', name: 'PAYPAL_CHECKOUT' }, + { status: 'ACTIVE', name: 'PAYPAL_WALLET_VAULTING_ADVANCED' }, + { status: 'ACTIVE', name: 'QR_CODE' }, + { status: 'ACTIVE', name: 'SEND_INVOICE' }, + { status: 'ACTIVE', name: 'SEPA' }, + { status: 'ACTIVE', name: 'SUBSCRIPTIONS' }, + { status: 'ACTIVE', name: 'VAT_TAX' }, + { status: 'ACTIVE', name: 'WITHDRAW_FUNDS_TO_DOMESTIC_BANK' }, + ], + oauth_integrations: [ + { + integration_method: 'PAYPAL', + integration_type: 'OAUTH_THIRD_PARTY', + oauth_third_party: [{ + merchant_client_id: 'xxFgvoYN7vzQ5KsoB4J7T1-8ylwpdVxlXCT0v0bMOILlfa4zvV8CQk4GdRXRfkPx3n4Jer_RjOH7OBzJ', + partner_client_id: '2H9kWfD51juip11YX0IN7SCYMq_BxXpHUnZV9hS6jx0EBaAxvDWEzJGR6XhDAfoRgiMAQGalsW9UNmmB', + scopes: [ + 'https://uri.paypal.com/services/payments/delay-funds-disbursement', + 'https://uri.paypal.com/services/payments/realtimepayment', + 'https://uri.paypal.com/services/reporting/search/read', + 'https://uri.paypal.com/services/payments/refund', + 'https://uri.paypal.com/services/customer/merchant-integrations/read', + 'https://uri.paypal.com/services/disputes/update-seller', + 'https://uri.paypal.com/services/payments/payment/authcapture', + 'https://uri.paypal.com/services/billing-agreements', + 'https://uri.paypal.com/services/vault/payment-tokens/read', + 'https://uri.paypal.com/services/vault/payment-tokens/readwrite', + 'https://uri.paypal.com/services/disputes/read-seller', + 'https://uri.paypal.com/services/shipping/trackers/readwrite', + ], + }], + }, + ], + granted_permissions: [], + payments_receivable: true, + legal_name: "Example's Test Store", + primary_email: 'test@example.com', + primary_email_confirmed: true, + }, + capabilities: { + 'some-payment-method-id': 'active', + }, +} satisfies PayPal.Setting<'merchant_information'>; + +const NotLoggedIn = { + merchantIntegrations: null, + capabilities: { + 'some-payment-method-id': 'inactive', + }, +} satisfies PayPal.Setting<'merchant_information'>; + +const NonPPCP = { + ...Default, + merchantIntegrations: { + ...Default.merchantIntegrations, + products: Default.merchantIntegrations.products.filter(({ name }) => !name.includes('PPCP')), + capabilities: Default.merchantIntegrations.capabilities.filter(({ name }) => !name.includes('PAYPAL_CHECKOUT')), + }, +} satisfies PayPal.Setting<'merchant_information'>; + +const NonVault = { + ...Default, + merchantIntegrations: { + ...Default.merchantIntegrations, + products: Default.merchantIntegrations.products.filter(({ name }) => !name.includes('VAULTING')), // ! PPCP_CUSTOM.capabilities still includes VAULTING ! + capabilities: Default.merchantIntegrations.capabilities.filter(({ name }) => !name.includes('VAULTING')), + }, +} satisfies PayPal.Setting<'merchant_information'>; + +export default { + Default, + NotLoggedIn, + NonPPCP, + NonVault, +}; diff --git a/src/Resources/app/administration/src/app/store/settings.fixture.ts b/src/Resources/app/administration/src/app/store/settings.fixture.ts new file mode 100644 index 000000000..eacf53089 --- /dev/null +++ b/src/Resources/app/administration/src/app/store/settings.fixture.ts @@ -0,0 +1,90 @@ +import type * as PayPal from 'src/types'; + +const Default = { + 'SwagPayPal.settings.sandbox': false, + 'SwagPayPal.settings.intent': 'CAPTURE', + 'SwagPayPal.settings.submitCart': true, + 'SwagPayPal.settings.landingPage': 'NO_PREFERENCE', + 'SwagPayPal.settings.sendOrderNumber': true, + 'SwagPayPal.settings.ecsDetailEnabled': true, + 'SwagPayPal.settings.ecsCartEnabled': true, + 'SwagPayPal.settings.ecsOffCanvasEnabled': true, + 'SwagPayPal.settings.ecsLoginEnabled': true, + 'SwagPayPal.settings.ecsListingEnabled': false, + 'SwagPayPal.settings.ecsButtonColor': 'gold', + 'SwagPayPal.settings.ecsButtonShape': 'sharp', + 'SwagPayPal.settings.ecsShowPayLater': true, + 'SwagPayPal.settings.ecsButtonLanguageIso': null, + + 'SwagPayPal.settings.spbButtonColor': 'gold', + 'SwagPayPal.settings.spbButtonShape': 'sharp', + 'SwagPayPal.settings.spbButtonLanguageIso': null, + 'SwagPayPal.settings.spbShowPayLater': true, + 'SwagPayPal.settings.spbCheckoutEnabled': true, + 'SwagPayPal.settings.spbAlternativePaymentMethodsEnabled': false, + + 'SwagPayPal.settings.installmentBannerDetailPageEnabled': true, + 'SwagPayPal.settings.installmentBannerCartEnabled': true, + 'SwagPayPal.settings.installmentBannerOffCanvasCartEnabled': true, + 'SwagPayPal.settings.installmentBannerLoginPageEnabled': true, + 'SwagPayPal.settings.installmentBannerFooterEnabled': true, + + 'SwagPayPal.settings.vaultingEnabledWallet': false, + 'SwagPayPal.settings.vaultingEnabledACDC': false, + 'SwagPayPal.settings.vaultingEnabledVenmo': false, + + 'SwagPayPal.settings.acdcForce3DS': false, + + 'SwagPayPal.settings.excludedProductIds': [], + 'SwagPayPal.settings.excludedProductStreamIds': [], + + 'SwagPayPal.settings.crossBorderMessagingEnabled': false, + 'SwagPayPal.settings.crossBorderBuyerCountry': null, + + /** + * @deprecated tag:v10.0.0 - Will be removed without replacement. + */ + 'SwagPayPal.settings.merchantLocation': 'other', + + /** + * @deprecated tag:v10.0.0 - Will be removed without replacement. + */ + 'SwagPayPal.settings.plusCheckoutEnabled': false, +} satisfies PayPal.SystemConfig; + +const All = { + ...Default, + 'SwagPayPal.settings.clientId': '', + 'SwagPayPal.settings.clientSecret': '', + 'SwagPayPal.settings.clientIdSandbox': '', + 'SwagPayPal.settings.clientSecretSandbox': '', + 'SwagPayPal.settings.merchantPayerId': '', + 'SwagPayPal.settings.merchantPayerIdSandbox': '', + + 'SwagPayPal.settings.webhookId': '', + 'SwagPayPal.settings.webhookExecuteToken': '', + 'SwagPayPal.settings.brandName': '', + 'SwagPayPal.settings.orderNumberPrefix': '', + 'SwagPayPal.settings.orderNumberSuffix': '', + + 'SwagPayPal.settings.puiCustomerServiceInstructions': '', + + 'SwagPayPal.settings.vaultingEnabled': false, + 'SwagPayPal.settings.vaultingEnableAlways': false, +} satisfies Required; + +const WithCredentials = { + ...Default, + 'SwagPayPal.settings.clientId': 'some-client-id', + 'SwagPayPal.settings.clientSecret': 'some-client-secret', + 'SwagPayPal.settings.merchantPayerId': 'some-merchant-payer-id', +}; + +const WithSandboxCredentials = { + ...Default, + 'SwagPayPal.settings.clientIdSandbox': 'some-client-id-sandbox', + 'SwagPayPal.settings.clientSecretSandbox': 'some-client-secret-sandbox', + 'SwagPayPal.settings.merchantPayerIdSandbox': 'some-merchant-payer-id-sandbox', +}; + +export default { Default, All, WithCredentials, WithSandboxCredentials }; diff --git a/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.spec.ts b/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.spec.ts new file mode 100644 index 000000000..8c7c6316f --- /dev/null +++ b/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.spec.ts @@ -0,0 +1,111 @@ +import MIFixture from './merchant-information.fixture'; +import './swag-paypal-merchant-information.store'; + +describe('swag-paypal-merchant-information.store', () => { + const store = Shopware.Store.get('swagPayPalMerchantInformation'); + + it('shoud be a pinia store', () => { + expect(store.$id).toBe('swagPayPalMerchantInformation'); + }); + + it('should have correct default state', () => { + // state + expect(store.salesChannel).toBeNull(); + expect(store.allMerchantInformations).toStrictEqual({}); + + // actions + expect(store.has(null)).toBe(false); + + // getters + expect(store.isLoading).toBe(true); + expect(store.actual).toStrictEqual({ + merchantIntegrations: null, + capabilities: {}, + }); + expect(store.products).toStrictEqual([]); + expect(store.capabilities).toStrictEqual({}); + expect(store.merchantCapabilities).toStrictEqual([]); + expect(store.canVault).toBe(false); + expect(store.canPPCP).toBe(false); + }); + + it('should have correct root state', () => { + store.set(null, MIFixture.Default); + + // state + expect(store.salesChannel).toBeNull(); + expect(store.allMerchantInformations).toStrictEqual({ null: MIFixture.Default }); + + // actions + expect(store.has(null)).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.actual).toStrictEqual(MIFixture.Default); + expect(store.products).toStrictEqual(MIFixture.Default.merchantIntegrations.products); + expect(store.capabilities).toStrictEqual(MIFixture.Default.capabilities); + expect(store.merchantCapabilities).toStrictEqual(MIFixture.Default.merchantIntegrations.capabilities); + expect(store.canVault).toBe(true); + expect(store.canPPCP).toBe(true); + }); + + it('should have correct non-vault state', () => { + store.set(null, MIFixture.NonVault); + + // state + expect(store.salesChannel).toBeNull(); + expect(store.allMerchantInformations).toStrictEqual({ null: MIFixture.NonVault }); + + // actions + expect(store.has(null)).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.actual).toStrictEqual(MIFixture.NonVault); + expect(store.products).toStrictEqual(MIFixture.NonVault.merchantIntegrations.products); + expect(store.capabilities).toStrictEqual(MIFixture.NonVault.capabilities); + expect(store.merchantCapabilities).toStrictEqual(MIFixture.NonVault.merchantIntegrations.capabilities); + expect(store.canVault).toBe(false); + expect(store.canPPCP).toBe(true); + }); + + it('should have correct non-ppcp state', () => { + store.set(null, MIFixture.NonPPCP); + + // state + expect(store.salesChannel).toBeNull(); + expect(store.allMerchantInformations).toStrictEqual({ null: MIFixture.NonPPCP }); + + // actions + expect(store.has(null)).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.actual).toStrictEqual(MIFixture.NonPPCP); + expect(store.products).toStrictEqual(MIFixture.NonPPCP.merchantIntegrations.products); + expect(store.capabilities).toStrictEqual(MIFixture.NonPPCP.capabilities); + expect(store.merchantCapabilities).toStrictEqual(MIFixture.NonPPCP.merchantIntegrations.capabilities); + expect(store.canVault).toBe(true); + expect(store.canPPCP).toBe(false); + }); + + it('should have correct not-logged-in state', () => { + store.set(null, MIFixture.NotLoggedIn); + + // state + expect(store.salesChannel).toBeNull(); + expect(store.allMerchantInformations).toStrictEqual({ null: MIFixture.NotLoggedIn }); + + // actions + expect(store.has(null)).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.actual).toStrictEqual(MIFixture.NotLoggedIn); + expect(store.products).toStrictEqual([]); + expect(store.capabilities).toStrictEqual(MIFixture.NotLoggedIn.capabilities); + expect(store.merchantCapabilities).toStrictEqual([]); + expect(store.canVault).toBe(false); + expect(store.canPPCP).toBe(false); + }); +}); diff --git a/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.ts b/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.ts new file mode 100644 index 000000000..52416c8af --- /dev/null +++ b/src/Resources/app/administration/src/app/store/swag-paypal-merchant-information.store.ts @@ -0,0 +1,69 @@ +import type * as PayPal from 'src/types'; + +type State = { + salesChannel: string | null; + allMerchantInformations: Record>; +}; + +const store = Shopware.Store.register({ + id: 'swagPayPalMerchantInformation', + + state: (): State => ({ + salesChannel: null, + allMerchantInformations: {}, + }), + + actions: { + set(salesChannelId: string | null, merchantInformation: PayPal.Setting<'merchant_information'>) { + this.allMerchantInformations[String(salesChannelId)] = merchantInformation; + }, + + has(salesChannelId: string | null): boolean { + return this.allMerchantInformations.hasOwnProperty(String(salesChannelId)); + }, + + delete(salesChannelId: string | null) { + delete this.allMerchantInformations[String(salesChannelId)]; + }, + }, + + getters: { + isLoading(): boolean { + return !this.allMerchantInformations.hasOwnProperty(String(this.salesChannel)); + }, + + actual(): PayPal.Setting<'merchant_information'> { + return this.allMerchantInformations[String(this.salesChannel)] ?? { + merchantIntegrations: null, + capabilities: {}, + }; + }, + + products(): PayPal.V1<'merchant_integrations'>['products'] { + return this.actual.merchantIntegrations?.products ?? []; + }, + + capabilities(): PayPal.Setting<'merchant_information'>['capabilities'] { + return this.actual.capabilities; + }, + + merchantCapabilities(): NonNullable['capabilities']> { + return this.actual.merchantIntegrations?.capabilities ?? []; + }, + + canVault(): boolean { + return this.merchantCapabilities.some( + (capability) => capability.name === 'PAYPAL_WALLET_VAULTING_ADVANCED' && capability.status === 'ACTIVE', + ); + }, + + canPPCP(): boolean { + return this.merchantCapabilities.some( + (capability) => capability.name === 'PAYPAL_CHECKOUT' && capability.status === 'ACTIVE', + ); + }, + }, +}); + +export type MerchantInformationStore = ReturnType; +export default store; diff --git a/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.spec.ts b/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.spec.ts new file mode 100644 index 000000000..ad153fb36 --- /dev/null +++ b/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.spec.ts @@ -0,0 +1,118 @@ +import SettingsFixture from './settings.fixture'; +import './swag-paypal-settings.store'; + +describe('swag-paypal-settings.store', () => { + const store = Shopware.Store.get('swagPayPalSettings'); + + it('shoud be a pinia store', () => { + expect(store.$id).toBe('swagPayPalSettings'); + }); + + it('should have correct default state', () => { + // state + expect(store.salesChannel).toBeNull(); + expect(store.allConfigs).toStrictEqual({}); + + // actions + expect(store.hasConfig(null)).toBe(false); + + // getters + expect(store.isLoading).toBe(true); + expect(store.isSandbox).toBe(false); + expect(store.root).toStrictEqual({}); + expect(store.actual).toStrictEqual({}); + }); + + it('should have correct root state', () => { + store.setConfig(null, SettingsFixture.Default); + + // state + expect(store.salesChannel).toBeNull(); + expect(store.allConfigs).toStrictEqual({ null: SettingsFixture.Default }); + + // actions + expect(store.hasConfig(null)).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.isSandbox).toBe(false); + expect(store.root).toStrictEqual(SettingsFixture.Default); + expect(store.actual).toStrictEqual(SettingsFixture.Default); + }); + + it('should have correct actual state', () => { + const actual = { 'SwagPayPal.settings.sandbox': true }; + + store.setConfig(null, SettingsFixture.Default); + store.salesChannel = 'some-other-id'; + store.setConfig('some-other-id', actual); + + // state + expect(store.salesChannel).toBe('some-other-id'); + expect(store.allConfigs).toStrictEqual({ + null: SettingsFixture.Default, + 'some-other-id': actual, + }); + + // actions + expect(store.hasConfig(null)).toBe(true); + expect(store.hasConfig('some-other-id')).toBe(true); + + // getters + expect(store.isLoading).toBe(false); + expect(store.isSandbox).toBe(true); + expect(store.root).toStrictEqual(SettingsFixture.Default); + expect(store.actual).toStrictEqual(actual); + }); + + it('should have inherit correctly with root value', () => { + store.setConfig(null, SettingsFixture.Default); + store.salesChannel = 'some-other-id'; + store.setConfig('some-other-id', {}); + + const key = 'SwagPayPal.settings.intent'; + + expect(store.get(key)).toBe('CAPTURE'); + expect(store.getRoot(key)).toBe('CAPTURE'); + expect(store.getActual(key)).toBeUndefined(); + + store.set(key, 'AUTORIZE'); + expect(store.get(key)).toBe('AUTORIZE'); + expect(store.getRoot(key)).toBe('CAPTURE'); + expect(store.getActual(key)).toBe('AUTORIZE'); + }); + + it('should have inherit correctly without root value', () => { + store.setConfig(null, SettingsFixture.Default); + store.salesChannel = 'some-other-id'; + store.setConfig('some-other-id', {}); + + const key = 'SwagPayPal.settings.clientId'; + + expect(store.get(key)).toBeUndefined(); + expect(store.getRoot(key)).toBeUndefined(); + expect(store.getActual(key)).toBeUndefined(); + + store.set(key, 'some-client-id'); + expect(store.get(key)).toBe('some-client-id'); + expect(store.getRoot(key)).toBeUndefined(); + expect(store.getActual(key)).toBe('some-client-id'); + }); + + it('should have inherit correctly with root NULL value', () => { + store.setConfig(null, SettingsFixture.Default); + store.salesChannel = 'some-other-id'; + store.setConfig('some-other-id', {}); + + const key = 'SwagPayPal.settings.crossBorderBuyerCountry'; + + expect(store.get(key)).toBeUndefined(); + expect(store.getRoot(key)).toBeUndefined(); + expect(store.getActual(key)).toBeUndefined(); + + store.set(key, 'de-DE'); + expect(store.get(key)).toBe('de-DE'); + expect(store.getRoot(key)).toBeUndefined(); + expect(store.getActual(key)).toBe('de-DE'); + }); +}); diff --git a/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.ts b/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.ts new file mode 100644 index 000000000..2edfd7126 --- /dev/null +++ b/src/Resources/app/administration/src/app/store/swag-paypal-settings.store.ts @@ -0,0 +1,63 @@ +import type * as PayPal from 'src/types'; + +type State = { + salesChannel: string | null; + allConfigs: Readonly>; +}; + +const store = Shopware.Store.register({ + id: 'swagPayPalSettings', + + state: (): State => ({ + salesChannel: null, + allConfigs: {}, + }), + + actions: { + setConfig(salesChannelId: string | null, config: PayPal.SystemConfig) { + // @ts-expect-error - we are allowed to mutate the state + this.allConfigs[String(salesChannelId)] = config; + }, + + hasConfig(salesChannelId: string | null): boolean { + return this.allConfigs.hasOwnProperty(String(salesChannelId)); + }, + + set(key: K, value: PayPal.SystemConfig[K]) { + this.allConfigs[String(this.salesChannel)][key] = value ?? undefined; + }, + + get(key: K): PayPal.SystemConfig[K] { + return this.actual[key] ?? this.root[key] ?? undefined; + }, + + getRoot(key: K): PayPal.SystemConfig[K] { + return this.root[key] ?? undefined; + }, + + getActual(key: K): PayPal.SystemConfig[K] { + return this.actual[key] ?? undefined; + }, + }, + + getters: { + isLoading(): boolean { + return !this.allConfigs.hasOwnProperty(String(this.salesChannel)); + }, + + isSandbox(): boolean { + return this.actual['SwagPayPal.settings.sandbox'] ?? this.root['SwagPayPal.settings.sandbox'] ?? false; + }, + + root(): PayPal.SystemConfig { + return this.allConfigs.null ?? {}; + }, + + actual(): PayPal.SystemConfig { + return this.allConfigs[String(this.salesChannel)] ?? {}; + }, + }, +}); + +export type SettingsStore = ReturnType; +export default store; diff --git a/src/Resources/app/administration/src/constant/swag-paypal-settings.constant.ts b/src/Resources/app/administration/src/constant/swag-paypal-settings.constant.ts new file mode 100644 index 000000000..5d1e0502b --- /dev/null +++ b/src/Resources/app/administration/src/constant/swag-paypal-settings.constant.ts @@ -0,0 +1,155 @@ +export const LOCALES = [ + 'ar_EG', + 'cs_CZ', + 'da_DK', + 'de_DE', + 'el_GR', + 'en_AU', + 'en_GB', + 'en_IN', + 'en_US', + 'es_ES', + 'es_XC', + 'fi_FI', + 'fr_CA', + 'fr_FR', + 'fr_XC', + 'he_IL', + 'hu_HU', + 'id_ID', + 'it_IT', + 'ja_JP', + 'ko_KR', + 'nl_NL', + 'no_NO', + 'pl_PL', + 'pt_BR', + 'pt_PT', + 'ru_RU', + 'sk_SK', + 'sv_SE', + 'th_TH', + 'zh_CN', + 'zh_HK', + 'zh_TW', + 'zh_XC', +] as const; + +export type LOCALE = typeof LOCALES[number]; + +export const COUNTRY_OVERRIDES = [ + 'en-AU', + 'de-DE', + 'es-ES', + 'fr-FR', + 'en-GB', + 'it-IT', + 'en-US', +] as const; + +export type COUNTRY_OVERRIDE = typeof COUNTRY_OVERRIDES[number]; + +export const INTENTS = [ + 'CAPTURE', + 'AUTHORIZE', +] as const; + +export type INTENT = typeof INTENTS[number]; + +export const LANDING_PAGES = [ + 'LOGIN', + 'GUEST_CHECKOUT', + 'NO_PREFERENCE', +] as const; + +export type LANDING_PAGE = typeof LANDING_PAGES[number]; + +export const BUTTON_COLORS = [ + 'blue', + 'black', + 'gold', + 'silver', + 'white', +] as const; + +export type BUTTON_COLOR = typeof BUTTON_COLORS[number]; + +export const BUTTON_SHAPES = [ + 'sharp', + 'pill', + 'rect', +] as const; + +export type BUTTON_SHAPE = typeof BUTTON_SHAPES[number]; + +export const SYSTEM_CONFIGS = [ + 'SwagPayPal.settings.clientId', + 'SwagPayPal.settings.clientSecret', + 'SwagPayPal.settings.clientIdSandbox', + 'SwagPayPal.settings.clientSecretSandbox', + 'SwagPayPal.settings.merchantPayerId', + 'SwagPayPal.settings.merchantPayerIdSandbox', + 'SwagPayPal.settings.sandbox', + + 'SwagPayPal.settings.intent', + 'SwagPayPal.settings.submitCart', + 'SwagPayPal.settings.brandName', + 'SwagPayPal.settings.landingPage', + 'SwagPayPal.settings.sendOrderNumber', + 'SwagPayPal.settings.orderNumberPrefix', + 'SwagPayPal.settings.orderNumberSuffix', + 'SwagPayPal.settings.excludedProductIds', + 'SwagPayPal.settings.excludedProductStreamIds', + + 'SwagPayPal.settings.ecsDetailEnabled', + 'SwagPayPal.settings.ecsCartEnabled', + 'SwagPayPal.settings.ecsOffCanvasEnabled', + 'SwagPayPal.settings.ecsLoginEnabled', + 'SwagPayPal.settings.ecsListingEnabled', + 'SwagPayPal.settings.ecsButtonColor', + 'SwagPayPal.settings.ecsButtonShape', + 'SwagPayPal.settings.ecsButtonLanguageIso', + 'SwagPayPal.settings.ecsShowPayLater', + + 'SwagPayPal.settings.spbButtonColor', + 'SwagPayPal.settings.spbButtonShape', + 'SwagPayPal.settings.spbButtonLanguageIso', + 'SwagPayPal.settings.spbShowPayLater', + 'SwagPayPal.settings.spbCheckoutEnabled', + 'SwagPayPal.settings.spbAlternativePaymentMethodsEnabled', + + 'SwagPayPal.settings.acdcForce3DS', + + 'SwagPayPal.settings.puiCustomerServiceInstructions', + + 'SwagPayPal.settings.installmentBannerDetailPageEnabled', + 'SwagPayPal.settings.installmentBannerCartEnabled', + 'SwagPayPal.settings.installmentBannerOffCanvasCartEnabled', + 'SwagPayPal.settings.installmentBannerLoginPageEnabled', + 'SwagPayPal.settings.installmentBannerFooterEnabled', + + 'SwagPayPal.settings.vaultingEnabled', + 'SwagPayPal.settings.vaultingEnableAlways', + 'SwagPayPal.settings.vaultingEnabledWallet', + 'SwagPayPal.settings.vaultingEnabledACDC', + 'SwagPayPal.settings.vaultingEnabledVenmo', + + 'SwagPayPal.settings.crossBorderMessagingEnabled', + 'SwagPayPal.settings.crossBorderBuyerCountry', + + 'SwagPayPal.settings.webhookId', + 'SwagPayPal.settings.webhookExecuteToken', + + + /** + * @deprecated tag:v10.0.0 - Will be removed without replacement. + */ + 'SwagPayPal.settings.merchantLocation', + + /** + * @deprecated tag:v10.0.0 - Will be removed without replacement. + */ + 'SwagPayPal.settings.plusCheckoutEnabled', +] as const; + +export type SYSTEM_CONFIG = typeof SYSTEM_CONFIGS[number]; diff --git a/src/Resources/app/administration/src/core/service/api/swag-paypal-api-credentials.service.ts b/src/Resources/app/administration/src/core/service/api/swag-paypal-api-credentials.service.ts index 4fb2e5a8f..c68c09b3e 100644 --- a/src/Resources/app/administration/src/core/service/api/swag-paypal-api-credentials.service.ts +++ b/src/Resources/app/administration/src/core/service/api/swag-paypal-api-credentials.service.ts @@ -4,6 +4,9 @@ import type * as PayPal from 'src/types'; const ApiService = Shopware.Classes.ApiService; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `SwagPayPalSettingsService` + */ class SwagPayPalApiCredentialsService extends ApiService { constructor(httpClient: AxiosInstance, loginService: LoginService, apiEndpoint = 'paypal') { super(httpClient, loginService, apiEndpoint); diff --git a/src/Resources/app/administration/src/core/service/api/swag-paypal-settings.service.ts b/src/Resources/app/administration/src/core/service/api/swag-paypal-settings.service.ts new file mode 100644 index 000000000..615bf70f3 --- /dev/null +++ b/src/Resources/app/administration/src/core/service/api/swag-paypal-settings.service.ts @@ -0,0 +1,52 @@ +import type { LoginService } from 'src/core/service/login.service'; +import type { AxiosInstance } from 'axios'; +import type * as PayPal from 'src/types'; + +const ApiService = Shopware.Classes.ApiService; + +export default class SwagPayPalSettingsService extends ApiService { + constructor(httpClient: AxiosInstance, loginService: LoginService, apiEndpoint = 'paypal') { + super(httpClient, loginService, apiEndpoint); + } + + save(allConfigs: Record) { + return this.httpClient.post>( + `_action/${this.getApiBasePath()}/save-settings`, + allConfigs, + { headers: this.getBasicHeaders() }, + ).then(ApiService.handleResponse.bind(this)); + } + + testApiCredentials(clientId?: string, clientSecret?: string, merchantPayerId?: string, sandboxActive?: boolean) { + return this.httpClient.post>( + `_action/${this.getApiBasePath()}/test-api-credentials`, + { clientId, clientSecret, sandboxActive, merchantPayerId }, + { headers: this.getBasicHeaders() }, + ).then(ApiService.handleResponse.bind(this)); + } + + getApiCredentials( + authCode: string, + sharedId: string, + nonce: string, + sandboxActive: boolean, + params: object = {}, + additionalHeaders: object = {}, + ) { + return this.httpClient.post>( + `_action/${this.getApiBasePath()}/get-api-credentials`, + { authCode, sharedId, nonce, sandboxActive }, + { params, headers: this.getBasicHeaders(additionalHeaders) }, + ).then(ApiService.handleResponse.bind(this)); + } + + getMerchantInformation(salesChannelId: string | null = null) { + return this.httpClient.get>( + `_action/${this.getApiBasePath()}/merchant-information`, + { + params: { salesChannelId }, + headers: this.getBasicHeaders(), + }, + ).then(ApiService.handleResponse.bind(this)); + } +} diff --git a/src/Resources/app/administration/src/global.types.ts b/src/Resources/app/administration/src/global.types.ts index 0794a8b53..cd27c96b6 100644 --- a/src/Resources/app/administration/src/global.types.ts +++ b/src/Resources/app/administration/src/global.types.ts @@ -6,6 +6,8 @@ import type SwagPaypalNotificationMixin from './mixin/swag-paypal-notification.m import type SwagPaypalCredentialsLoaderMixin from './mixin/swag-paypal-credentials-loader.mixin'; import type SwagPaypalPosCatchErrorMixin from './mixin/swag-paypal-pos-catch-error.mixin'; import type SwagPaypalPosLogLabelMixin from './mixin/swag-paypal-pos-log-label.mixin'; +import type SwagPaypalSettingsMixin from './mixin/swag-paypal-settings.mixin'; +import type SwagPaypalMerchantInformationMixin from './mixin/swag-paypal-merchant-information.mixin'; import type SwagPayPalApiCredentialsService from './core/service/api/swag-paypal-api-credentials.service'; import type SwagPayPalDisputeApiService from './core/service/api/swag-paypal-dispute.api.service'; import type SwagPayPalOrderService from './core/service/api/swag-paypal-order.service'; @@ -15,6 +17,9 @@ import type SwagPayPalPosSettingApiService from './core/service/api/swag-paypal- import type SwagPayPalPosWebhookRegisterService from './core/service/api/swag-paypal-pos-webhook-register.service'; import type SwagPayPalPosApiService from './core/service/api/swag-paypal-pos.api.service'; import type SwagPayPalWebhookService from './core/service/api/swag-paypal-webhook.service'; +import type SwagPayPalSettingsService from './core/service/api/swag-paypal-settings.service'; +import type { MerchantInformationStore } from './app/store/swag-paypal-merchant-information.store'; +import type { SettingsStore } from './app/store/swag-paypal-settings.store'; declare global { type TEntity = Entity; @@ -28,6 +33,8 @@ declare global { 'swag-paypal-notification': typeof SwagPaypalNotificationMixin; 'swag-paypal-pos-catch-error': typeof SwagPaypalPosCatchErrorMixin; 'swag-paypal-pos-log-label': typeof SwagPaypalPosLogLabelMixin; + 'swag-paypal-settings': typeof SwagPaypalSettingsMixin; + 'swag-paypal-merchant-information': typeof SwagPaypalMerchantInformationMixin; } interface ServiceContainer { @@ -40,6 +47,12 @@ declare global { SwagPayPalOrderService: SwagPayPalOrderService; SwagPaypalPaymentMethodService: SwagPaypalPaymentMethodService; SwagPayPalDisputeApiService: SwagPayPalDisputeApiService; + SwagPayPalSettingsService: SwagPayPalSettingsService; + } + + interface PiniaRootState { + swagPayPalMerchantInformation: MerchantInformationStore; + swagPayPalSettings: SettingsStore; } } diff --git a/src/Resources/app/administration/src/init/api-service.init.ts b/src/Resources/app/administration/src/init/api-service.init.ts index 046f40529..59bcfdcc6 100644 --- a/src/Resources/app/administration/src/init/api-service.init.ts +++ b/src/Resources/app/administration/src/init/api-service.init.ts @@ -7,6 +7,7 @@ import SwagPayPalPaymentService from '../core/service/api/swag-paypal-payment.se import SwagPayPalOrderService from '../core/service/api/swag-paypal-order.service'; import SwagPaypalPaymentMethodService from '../core/service/api/swag-paypal-payment-method.service'; import SwagPayPalDisputeApiService from '../core/service/api/swag-paypal-dispute.api.service'; +import SwagPayPalSettingsService from '../core/service/api/swag-paypal-settings.service'; const { Application } = Shopware; @@ -56,3 +57,7 @@ Application.addServiceProvider( 'SwagPayPalDisputeApiService', (container) => new SwagPayPalDisputeApiService(initContainer.httpClient, container.loginService), ); +Application.addServiceProvider( + 'SwagPayPalSettingsService', + (container) => new SwagPayPalSettingsService(initContainer.httpClient, container.loginService), +); diff --git a/src/Resources/app/administration/src/main.ts b/src/Resources/app/administration/src/main.ts index fcd80df91..94146d844 100644 --- a/src/Resources/app/administration/src/main.ts +++ b/src/Resources/app/administration/src/main.ts @@ -4,9 +4,10 @@ import './mixin/swag-paypal-credentials-loader.mixin'; import './mixin/swag-paypal-notification.mixin'; import './mixin/swag-paypal-pos-catch-error.mixin'; import './mixin/swag-paypal-pos-log-label.mixin'; +import './mixin/swag-paypal-settings.mixin'; +import './mixin/swag-paypal-merchant-information.mixin'; import './module/extension'; -import './module/swag-paypal'; import './module/swag-paypal-disputes'; import './module/swag-paypal-payment'; import './module/swag-paypal-pos'; @@ -15,29 +16,51 @@ import './init/api-service.init'; import './init/translation.init'; import './init/svg-icons.init'; -ui.module.payment.overviewCard.add({ - positionId: 'swag-paypal-overview-card-before', - component: 'swag-paypal-overview-card', - paymentMethodHandlers: [ - 'handler_swag_trustlyapmhandler', - 'handler_swag_sofortapmhandler', - 'handler_swag_p24apmhandler', - 'handler_swag_oxxoapmhandler', - 'handler_swag_mybankapmhandler', - 'handler_swag_multibancoapmhandler', - 'handler_swag_idealapmhandler', - 'handler_swag_giropayapmhandler', - 'handler_swag_epsapmhandler', - 'handler_swag_blikapmhandler', - 'handler_swag_bancontactapmhandler', - 'handler_swag_sepahandler', - 'handler_swag_acdchandler', - 'handler_swag_puihandler', - 'handler_swag_paypalpaymenthandler', - 'handler_swag_pospayment', - 'handler_swag_venmohandler', - 'handler_swag_paylaterhandler', - 'handler_swag_applepayhandler', - 'handler_swag_googlepayhandler', - ], -}); +const bootPromise = window.Shopware ? Shopware.Plugin.addBootPromise() : () => {}; + +(async () => { + if (Shopware.Feature.isActive('PAYPAL_SETTINGS_TWEAKS')) { + await import('./app'); + // @ts-expect-error - yes it's not a module + await import('./module/swag-paypal-settings'); + // @ts-expect-error - yes it's not a module + await import('./module/swag-paypal-method'); + } else { + await import('./module/swag-paypal'); + } + + // @ts-expect-error - bootPromise has a wrong doc type + bootPromise(); +})(); + +if (!Shopware.Feature.isActive('PAYPAL_SETTINGS_TWEAKS')) { + /** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-method` + */ + ui.module.payment.overviewCard.add({ + positionId: 'swag-paypal-overview-card-before', + component: 'swag-paypal-overview-card', + paymentMethodHandlers: [ + 'handler_swag_trustlyapmhandler', + 'handler_swag_sofortapmhandler', + 'handler_swag_p24apmhandler', + 'handler_swag_oxxoapmhandler', + 'handler_swag_mybankapmhandler', + 'handler_swag_multibancoapmhandler', + 'handler_swag_idealapmhandler', + 'handler_swag_giropayapmhandler', + 'handler_swag_epsapmhandler', + 'handler_swag_blikapmhandler', + 'handler_swag_bancontactapmhandler', + 'handler_swag_sepahandler', + 'handler_swag_acdchandler', + 'handler_swag_puihandler', + 'handler_swag_paypalpaymenthandler', + 'handler_swag_pospayment', + 'handler_swag_venmohandler', + 'handler_swag_paylaterhandler', + 'handler_swag_applepayhandler', + 'handler_swag_googlepayhandler', + ], + }); +} diff --git a/src/Resources/app/administration/src/mixin/swag-paypal-credentials-loader.mixin.ts b/src/Resources/app/administration/src/mixin/swag-paypal-credentials-loader.mixin.ts index edce8b91d..75afad4d7 100644 --- a/src/Resources/app/administration/src/mixin/swag-paypal-credentials-loader.mixin.ts +++ b/src/Resources/app/administration/src/mixin/swag-paypal-credentials-loader.mixin.ts @@ -1,5 +1,8 @@ const { debug } = Shopware.Utils; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-onboarding-button` + */ export default Shopware.Mixin.register('swag-paypal-credentials-loader', Shopware.Component.wrapComponentConfig({ inject: ['SwagPayPalApiCredentialsService'], diff --git a/src/Resources/app/administration/src/mixin/swag-paypal-merchant-information.mixin.ts b/src/Resources/app/administration/src/mixin/swag-paypal-merchant-information.mixin.ts new file mode 100644 index 000000000..ec5aee2a7 --- /dev/null +++ b/src/Resources/app/administration/src/mixin/swag-paypal-merchant-information.mixin.ts @@ -0,0 +1,56 @@ +import type * as PayPal from 'src/types'; + +export default Shopware.Mixin.register('swag-paypal-merchant-information', Shopware.Component.wrapComponentConfig({ + inject: [ + 'SwagPayPalApiCredentialsService', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-notification'), + ], + + computed: { + merchantInformationStore() { + return Shopware.Store.get('swagPayPalMerchantInformation'); + }, + }, + + watch: { + 'merchantInformationStore.salesChannel': { + immediate: true, + handler(salesChannel: string | null) { + this.fetchMerchantInformation(salesChannel); + }, + }, + 'savingSettings'(savingSettings: string) { + if (savingSettings === 'success') { + this.fetchMerchantInformation(this.merchantInformationStore.salesChannel); + } + }, + }, + + beforeCreate() { + // computed properties aren't ready yet + Shopware.Store.get('swagPayPalMerchantInformation').$reset(); + }, + + methods: { + async fetchMerchantInformation(salesChannel: string | null) { + if (this.merchantInformationStore.has(salesChannel)) { + return; + } + + const merchantInformation = await this.SwagPayPalApiCredentialsService + .getMerchantInformation(salesChannel) + .catch((errorResponse: PayPal.ServiceError) => { + this.createNotificationFromError({ errorResponse }); + + return null; + }); + + if (merchantInformation) { + this.merchantInformationStore.set(salesChannel, merchantInformation); + } + }, + }, +})); diff --git a/src/Resources/app/administration/src/mixin/swag-paypal-notification.mixin.ts b/src/Resources/app/administration/src/mixin/swag-paypal-notification.mixin.ts index 398b8d3c1..17f5bf067 100644 --- a/src/Resources/app/administration/src/mixin/swag-paypal-notification.mixin.ts +++ b/src/Resources/app/administration/src/mixin/swag-paypal-notification.mixin.ts @@ -1,4 +1,5 @@ import type * as PayPal from 'src/types'; +import type { HttpError } from "src/types"; /** * Options to handle a service error. @@ -26,6 +27,8 @@ export default Shopware.Mixin.register('swag-paypal-notification', Shopware.Comp methods: { /** + * @deprecated tag:v10.0.0 - Will be removed, use `createMessageFromError` instead + * * Handles a service error and creates a notification for each error. * If the errorResponse is undefined, a generic error notification will be created. * If the errorResponse is not a ShopwareHttpError, the errorResponse will be used as message. @@ -68,5 +71,26 @@ export default Shopware.Mixin.register('swag-paypal-notification', Shopware.Comp this.createNotificationError({ message: messages[i], title }); } }, + + /** + * Creates a message from a http error. + * Can handle axios responses, plain object containing errors or an array of errors. + */ + createMessageFromError(httpError: PayPal.ServiceError&{ errors?: HttpError[] }): string { + const errors = httpError.errors ?? httpError?.response?.data?.errors ?? []; + + const messages = errors.map((error) => { + const message = typeof error.meta?.parameters?.message === 'string' + ? error.meta.parameters.message + : error.detail; + + const snippet = `swag-paypal.errors.${error.code}`; + const translation = this.$t(snippet, { message }); + + return snippet !== translation ? translation : message; + }); + + return messages.join('
'); + }, }, })); diff --git a/src/Resources/app/administration/src/mixin/swag-paypal-settings.mixin.ts b/src/Resources/app/administration/src/mixin/swag-paypal-settings.mixin.ts new file mode 100644 index 000000000..14d67f46d --- /dev/null +++ b/src/Resources/app/administration/src/mixin/swag-paypal-settings.mixin.ts @@ -0,0 +1,92 @@ +import type * as PayPal from 'src/types'; + +export default Shopware.Mixin.register('swag-paypal-settings', Shopware.Component.wrapComponentConfig({ + inject: [ + 'systemConfigApiService', + 'SwagPayPalSettingsService', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-notification'), + ], + + data(): { + savingSettings: 'none' | 'loading' | 'success'; + } { + return { + savingSettings: 'none', + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + }, + + watch: { + 'settingsStore.salesChannel': { + immediate: true, + handler(salesChannel: string | null) { + this.fetchSettings(salesChannel); + }, + }, + }, + + beforeCreate() { + // computed properties aren't ready yet + Shopware.Store.get('swagPayPalSettings').$reset(); + }, + + methods: { + async fetchSettings(salesChannel: string | null): Promise { + if (this.settingsStore.hasConfig(salesChannel)) { + return; + } + + const config = await this.systemConfigApiService.getValues('SwagPayPal.settings', salesChannel as null) as PayPal.SystemConfig; + + this.settingsStore.setConfig(salesChannel, config || {}); + }, + + async saveSettings(): Promise> | void> { + this.savingSettings = 'loading'; + + return this.SwagPayPalSettingsService.save(this.settingsStore.allConfigs) + .then((response) => { + Object.entries(response).forEach(([salesChannel, information]) => { + this.handleSettingsSaveInformation(salesChannel, information); + }); + + this.savingSettings = 'success'; + + setTimeout(() => { this.savingSettings = 'none'; }, 5000); + + return response; + }) + .catch((error: PayPal.ServiceError) => { + this.createNotificationError({ + title: this.$t('swag-paypal.notifications.save.title'), + message: this.$t('swag-paypal.notifications.save.errorMessage', { + message: this.createMessageFromError(error), + }), + }); + + this.savingSettings = 'none'; + }); + }, + + handleSettingsSaveInformation(salesChannel: string, information: PayPal.Setting<'settings_information'>) { + if (information.sandboxCredentialsChanged || information.liveCredentialsChanged) { + Shopware.Store.get('swagPayPalMerchantInformation').delete(salesChannel); + } + + information.webhookErrors.forEach((message) => { + this.createNotificationWarning({ + title: this.$t('swag-paypal.notifications.save.title'), + message: this.$t('swag-paypal.notifications.save.webhookMessage', { message }), + }); + }); + }, + }, +})); diff --git a/src/Resources/app/administration/src/module/extension/index.ts b/src/Resources/app/administration/src/module/extension/index.ts index 767461348..683fc247b 100644 --- a/src/Resources/app/administration/src/module/extension/index.ts +++ b/src/Resources/app/administration/src/module/extension/index.ts @@ -1,12 +1,18 @@ -Shopware.Component.override('sw-first-run-wizard-paypal-credentials', () => import('./sw-first-run-wizard/sw-first-run-wizard-paypal-credentials')); +if (Shopware.Feature.isActive('PAYPAL_SETTINGS_TWEAKS')) { + Shopware.Component.override('sw-first-run-wizard-paypal-credentials', () => import('./sw-first-run-wizard/sw-first-run-wizard-paypal-credentials')); +} else { + Shopware.Component.override('sw-first-run-wizard-paypal-credentials', () => import('./sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated')); +} Shopware.Component.override('sw-sales-channel-modal-detail', () => import('./sw-sales-channel-modal-detail')); Shopware.Component.override('sw-sales-channel-modal-grid', () => import('./sw-sales-channel-modal-grid')); -Shopware.Component.register('swag-paypal-overview-card', () => import('./sw-settings-payment/components/swag-paypal-overview-card')); -Shopware.Component.override('sw-settings-payment-detail', () => import('./sw-settings-payment/sw-settings-payment-detail')); -Shopware.Component.override('sw-settings-payment-list', () => import('./sw-settings-payment/sw-settings-payment-list')); +if (!Shopware.Feature.isActive('PAYPAL_SETTINGS_TWEAKS')) { + Shopware.Component.register('swag-paypal-overview-card', () => import('./sw-settings-payment/components/swag-paypal-overview-card')); + Shopware.Component.override('sw-settings-payment-detail', () => import('./sw-settings-payment/sw-settings-payment-detail')); + Shopware.Component.override('sw-settings-payment-list', () => import('./sw-settings-payment/sw-settings-payment-list')); +} Shopware.Component.override('sw-settings-shipping-detail', () => import('./sw-settings-shipping/sw-settings-shipping-detail')); diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/de-DE.json b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/de-DE.json index bc8128dd3..c8e9b6381 100644 --- a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/de-DE.json +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/de-DE.json @@ -1,6 +1,7 @@ { "swag-paypal-frw-credentials": { "buttonGetCredentials": "Hole API Zugangsdaten", + "buttonGetSandboxCredentials": "Hole Sandbox API Zugangsdaten", "textIntroPayPal": "Um PayPal zu nutzen müssen nur die API Zugangsdaten eingegeben werden.", "labelClientId": "Client-ID", "labelClientSecret": "Client-Secret", @@ -15,6 +16,7 @@ "messageFetchedError": " Bitte versuche es erneut oder nutze die erweiterten Einstellungen um die Zugangsdaten direkt einzugeben.", "textFetchedSuccessful": "Die Zugangsdaten wurden erfolgreich abgerufen.", "messageNoCredentials": "Es wurden keine Zugangsdaten hinterlegt.", + "messageInvalidCredentials": "Die Zugangsdaten sind ungültig.", "messageTestSuccess": "Die Zugangsdaten sind gültig." } } diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/en-GB.json b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/en-GB.json index dc3f91c36..8c0697dc2 100644 --- a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/en-GB.json +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/snippets/en-GB.json @@ -1,6 +1,7 @@ { "swag-paypal-frw-credentials": { "buttonGetCredentials": "Get API credentials", + "buttonGetSandboxCredentials": "Get sandbox API credentials", "textIntroPayPal": "To get PayPal up and running you only need to provide your PayPal API credentials.", "labelClientId": "Client ID", "labelClientSecret": "Client secret", @@ -15,6 +16,7 @@ "messageFetchedError": "Try again or use the advanced settings to provide your credentials.", "textFetchedSuccessful": "Credentials have been fetched.", "messageNoCredentials": "No credentials provided.", + "messageInvalidCredentials": "Credentials are invalid.", "messageTestSuccess": "Credentials are valid." } } diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/index.ts b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/index.ts new file mode 100644 index 000000000..c15319caa --- /dev/null +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/index.ts @@ -0,0 +1,169 @@ +import type * as PayPal from 'src/types'; +import template from './sw-first-run-wizard-paypal-credentials.html.twig'; +import './sw-first-run-wizard-paypal-credentials.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + inject: [ + 'systemConfigApiService', + 'SwagPaypalPaymentMethodService', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-notification'), + Shopware.Mixin.getByName('swag-paypal-credentials-loader'), + ], + + data() { + return { + config: {} as PayPal.SystemConfig, + isLoading: false, + setDefault: false, + }; + }, + + computed: { + sandboxMode() { + return this.config['SwagPayPal.settings.sandbox'] || false; + }, + + onboardingUrl() { + return this.sandboxMode ? this.onboardingUrlSandbox : this.onboardingUrlLive; + }, + + onboardingCallback() { + return this.sandboxMode ? 'onboardingCallbackSandbox' : 'onboardingCallbackLive'; + }, + + buttonConfig() { + const prev = this.$super('buttonConfig') as { key: string; action: () => Promise }[]; + + return prev.map((button) => { + if (button.key === 'next') { + button.action = this.onClickNext.bind(this); + } + + return button; + }); + }, + + credentialsProvided() { + return (!this.sandboxMode && this.credentialsProvidedLive) + || (this.sandboxMode && this.credentialsProvidedSandbox); + }, + + credentialsProvidedLive() { + return !!this.config['SwagPayPal.settings.clientId'] + && !!this.config['SwagPayPal.settings.clientSecret']; + }, + + credentialsProvidedSandbox() { + return !!this.config['SwagPayPal.settings.clientIdSandbox'] + && !!this.config['SwagPayPal.settings.clientSecretSandbox']; + }, + }, + + created() { + this.createdComponent(); + }, + + methods: { + createdComponent() { + this.$super('createdComponent'); + this.fetchPayPalConfig(); + }, + + onPayPalCredentialsLoadSuccess(clientId: string, clientSecret: string, merchantPayerId: string, sandbox: boolean) { + this.setConfig(clientId, clientSecret, merchantPayerId, sandbox); + }, + + onPayPalCredentialsLoadFailed(sandbox: boolean) { + this.setConfig('', '', '', sandbox); + this.createNotificationError({ + message: this.$tc('swag-paypal-frw-credentials.messageFetchedError'), + // @ts-expect-error - duration is not defined correctly + duration: 10000, + }); + }, + + setConfig(clientId: string, clientSecret: string, merchantPayerId: string, sandbox: boolean) { + const suffix = sandbox ? 'Sandbox' : ''; + this.$set(this.config, `SwagPayPal.settings.clientId${suffix}`, clientId); + this.$set(this.config, `SwagPayPal.settings.clientSecret${suffix}`, clientSecret); + this.$set(this.config, `SwagPayPal.settings.merchantPayerId${suffix}`, merchantPayerId); + }, + + async onClickNext(): Promise { + if (!this.credentialsProvided) { + this.createNotificationError({ + message: this.$tc('swag-paypal-frw-credentials.messageNoCredentials'), + }); + + return true; + } + + try { + // Do not test the credentials if they have been fetched from the PayPal api + if (!this.isGetCredentialsSuccessful) { + await this.testApiCredentials(); + } + + await this.saveConfig(); + + this.$emit('frw-redirect', 'sw.first.run.wizard.index.plugins'); + + return false; + } catch { + return true; + } + }, + + fetchPayPalConfig() { + this.isLoading = true; + return this.systemConfigApiService.getValues('SwagPayPal.settings', null) + .then((values: PayPal.SystemConfig) => { + this.config = values; + }) + .finally(() => { + this.isLoading = false; + }); + }, + + async saveConfig() { + this.isLoading = true; + await this.systemConfigApiService.saveValues(this.config, null); + + if (this.setDefault) { + await this.SwagPaypalPaymentMethodService.setDefaultPaymentForSalesChannel(); + } + + this.isLoading = false; + }, + + async testApiCredentials() { + this.isLoading = true; + + const sandbox = this.config['SwagPayPal.settings.sandbox'] ?? false; + const sandboxSetting = sandbox ? 'Sandbox' : ''; + const clientId = this.config[`SwagPayPal.settings.clientId${sandboxSetting}`]; + const clientSecret = this.config[`SwagPayPal.settings.clientSecret${sandboxSetting}`]; + + const response = await this.SwagPayPalApiCredentialsService + .validateApiCredentials(clientId, clientSecret, sandbox) + .catch((errorResponse: PayPal.ServiceError) => { + this.createNotificationFromError({ errorResponse, title: 'swag-paypal.settingForm.messageTestError' }); + + return { credentialsValid: false }; + }); + + this.isLoading = false; + + return response.credentialsValid ? Promise.resolve() : Promise.reject(); + }, + + onCredentialsChanged() { + this.isGetCredentialsSuccessful = null; + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.html.twig b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.html.twig new file mode 100644 index 000000000..2edd26d36 --- /dev/null +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.html.twig @@ -0,0 +1,122 @@ +{% block sw_first_run_wizard_paypal_credentials %} +
+ + {% block sw_first_run_wizard_paypal_credentials_inner %} + + + {% block sw_first_run_wizard_paypal_credentials_intro %} +

+ {{ $tc('swag-paypal-frw-credentials.textIntroPayPal') }} +

+ {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_sandbox %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_button_container %} +
+ + {% block sw_first_run_wizard_paypal_credentials_button %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_indicator %} +
+ +
+ {% endblock %} +
+ {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_client_id %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_client_secret %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_merchant_id %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_client_id_sandbox %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_client_secret_sandbox %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_merchant_id_sandbox %} + + {% endblock %} + + {% block sw_first_run_wizard_paypal_credentials_set_default %} + + {% endblock %} + {% endblock %} +
+{% endblock %} diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.scss b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.scss new file mode 100644 index 000000000..194384243 --- /dev/null +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials-deprecated/sw-first-run-wizard-paypal-credentials.scss @@ -0,0 +1,31 @@ +@import "~scss/variables"; + +.sw-first-run-wizard-paypal-credentials { + width: 100%; + + .sw-first-run-wizard-paypal-credentials__headerText { + font-weight: bold; + color: $color-darkgray-200; + margin-bottom: 22px; + } + + .sw-first-run-wizard-paypal-credentials__button-container { + display: flex; + align-items: center; + margin-bottom: 22px; + + .sw-first-run-wizard-paypal-credentials__indicator { + display: inline; + margin-left: 25px; + + .sw-first-run-wizard-paypal-credentials__icon-successful { + color: $color-emerald-500; + margin-top: -5px; + } + + .sw-first-run-wizard-paypal-credentials__text-indicator { + margin-left: 8px; + } + } + } +} diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/index.ts b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/index.ts index c15319caa..23a5b49db 100644 --- a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/index.ts +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/index.ts @@ -1,4 +1,3 @@ -import type * as PayPal from 'src/types'; import template from './sw-first-run-wizard-paypal-credentials.html.twig'; import './sw-first-run-wizard-paypal-credentials.scss'; @@ -6,36 +5,28 @@ export default Shopware.Component.wrapComponentConfig({ template, inject: [ - 'systemConfigApiService', 'SwagPaypalPaymentMethodService', + 'SwagPayPalSettingsService', ], mixins: [ Shopware.Mixin.getByName('swag-paypal-notification'), - Shopware.Mixin.getByName('swag-paypal-credentials-loader'), + Shopware.Mixin.getByName('swag-paypal-settings'), ], - data() { + data(): { + isLoading: boolean; + asDefault: boolean; + error: { detail: string; code: string } | null; + } { return { - config: {} as PayPal.SystemConfig, isLoading: false, - setDefault: false, + asDefault: false, + error: null, }; }, computed: { - sandboxMode() { - return this.config['SwagPayPal.settings.sandbox'] || false; - }, - - onboardingUrl() { - return this.sandboxMode ? this.onboardingUrlSandbox : this.onboardingUrlLive; - }, - - onboardingCallback() { - return this.sandboxMode ? 'onboardingCallbackSandbox' : 'onboardingCallbackLive'; - }, - buttonConfig() { const prev = this.$super('buttonConfig') as { key: string; action: () => Promise }[]; @@ -48,54 +39,38 @@ export default Shopware.Component.wrapComponentConfig({ }); }, - credentialsProvided() { - return (!this.sandboxMode && this.credentialsProvidedLive) - || (this.sandboxMode && this.credentialsProvidedSandbox); - }, - - credentialsProvidedLive() { - return !!this.config['SwagPayPal.settings.clientId'] - && !!this.config['SwagPayPal.settings.clientSecret']; - }, - - credentialsProvidedSandbox() { - return !!this.config['SwagPayPal.settings.clientIdSandbox'] - && !!this.config['SwagPayPal.settings.clientSecretSandbox']; + hasLiveCredentials() { + return !!this.settingsStore.get('SwagPayPal.settings.clientId') + && !!this.settingsStore.get('SwagPayPal.settings.clientSecret'); }, - }, - - created() { - this.createdComponent(); - }, - methods: { - createdComponent() { - this.$super('createdComponent'); - this.fetchPayPalConfig(); + hasSandboxCredentials() { + return !!this.settingsStore.get('SwagPayPal.settings.clientIdSandbox') + && !!this.settingsStore.get('SwagPayPal.settings.clientSecretSandbox'); }, - onPayPalCredentialsLoadSuccess(clientId: string, clientSecret: string, merchantPayerId: string, sandbox: boolean) { - this.setConfig(clientId, clientSecret, merchantPayerId, sandbox); + hasCredentials() { + return (!this.settingsStore.isSandbox && this.hasLiveCredentials) + || (this.settingsStore.isSandbox && this.hasSandboxCredentials); }, - onPayPalCredentialsLoadFailed(sandbox: boolean) { - this.setConfig('', '', '', sandbox); - this.createNotificationError({ - message: this.$tc('swag-paypal-frw-credentials.messageFetchedError'), - // @ts-expect-error - duration is not defined correctly - duration: 10000, - }); + inputsDisabled() { + return this.isLoading || this.settingsStore.isLoading || this.savingSettings === 'loading'; }, + }, - setConfig(clientId: string, clientSecret: string, merchantPayerId: string, sandbox: boolean) { - const suffix = sandbox ? 'Sandbox' : ''; - this.$set(this.config, `SwagPayPal.settings.clientId${suffix}`, clientId); - this.$set(this.config, `SwagPayPal.settings.clientSecret${suffix}`, clientSecret); - this.$set(this.config, `SwagPayPal.settings.merchantPayerId${suffix}`, merchantPayerId); + watch: { + 'settingsStore.allConfigs': { + deep: true, + handler() { + this.resetError(); + }, }, + }, + methods: { async onClickNext(): Promise { - if (!this.credentialsProvided) { + if (!this.hasCredentials) { this.createNotificationError({ message: this.$tc('swag-paypal-frw-credentials.messageNoCredentials'), }); @@ -103,67 +78,57 @@ export default Shopware.Component.wrapComponentConfig({ return true; } - try { - // Do not test the credentials if they have been fetched from the PayPal api - if (!this.isGetCredentialsSuccessful) { - await this.testApiCredentials(); - } - - await this.saveConfig(); - - this.$emit('frw-redirect', 'sw.first.run.wizard.index.plugins'); - - return false; - } catch { - return true; - } - }, - - fetchPayPalConfig() { this.isLoading = true; - return this.systemConfigApiService.getValues('SwagPayPal.settings', null) - .then((values: PayPal.SystemConfig) => { - this.config = values; - }) - .finally(() => { - this.isLoading = false; - }); - }, - async saveConfig() { - this.isLoading = true; - await this.systemConfigApiService.saveValues(this.config, null); + const information = (await this.saveSettings())?.null; + + const haveChanged = (!this.settingsStore.isSandbox && information?.liveCredentialsChanged) || (this.settingsStore.isSandbox && information?.sandboxCredentialsChanged); + let areValid = (!this.settingsStore.isSandbox && information?.liveCredentialsValid) || (this.settingsStore.isSandbox && information?.sandboxCredentialsValid); - if (this.setDefault) { - await this.SwagPaypalPaymentMethodService.setDefaultPaymentForSalesChannel(); + if (!haveChanged) { + areValid = await this.onTest(); } this.isLoading = false; - }, - async testApiCredentials() { - this.isLoading = true; + if (!areValid) { + this.error = { + detail: this.$tc('swag-paypal-frw-credentials.messageInvalidCredentials'), + code: 'ASD', + }; - const sandbox = this.config['SwagPayPal.settings.sandbox'] ?? false; - const sandboxSetting = sandbox ? 'Sandbox' : ''; - const clientId = this.config[`SwagPayPal.settings.clientId${sandboxSetting}`]; - const clientSecret = this.config[`SwagPayPal.settings.clientSecret${sandboxSetting}`]; + return true; + } - const response = await this.SwagPayPalApiCredentialsService - .validateApiCredentials(clientId, clientSecret, sandbox) - .catch((errorResponse: PayPal.ServiceError) => { - this.createNotificationFromError({ errorResponse, title: 'swag-paypal.settingForm.messageTestError' }); + this.$emit('frw-redirect', 'sw.first.run.wizard.index.plugins'); - return { credentialsValid: false }; - }); + if (this.asDefault) { + try { + await this.SwagPaypalPaymentMethodService.setDefaultPaymentForSalesChannel(this.settingsStore.salesChannel); + } catch { + return true; + } + } - this.isLoading = false; + return false; + }, - return response.credentialsValid ? Promise.resolve() : Promise.reject(); + resetError() { + this.error = null; }, - onCredentialsChanged() { - this.isGetCredentialsSuccessful = null; + async onTest() { + const suffix = this.settingsStore.isSandbox ? 'Sandbox' : ''; + + return this.SwagPayPalSettingsService + .testApiCredentials( + this.settingsStore.get(`SwagPayPal.settings.clientId${suffix}`), + this.settingsStore.get(`SwagPayPal.settings.clientSecret${suffix}`), + this.settingsStore.get(`SwagPayPal.settings.merchantPayerId${suffix}`), + this.settingsStore.isSandbox, + ) + .then((response) => response.valid) + .catch(() => false); }, }, }); diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.html.twig b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.html.twig index 2edd26d36..d35588e17 100644 --- a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.html.twig +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.html.twig @@ -1,122 +1,85 @@ {% block sw_first_run_wizard_paypal_credentials %}
+

+ {{ $tc('swag-paypal-frw-credentials.textIntroPayPal') }} +

- {% block sw_first_run_wizard_paypal_credentials_inner %} - - - {% block sw_first_run_wizard_paypal_credentials_intro %} -

- {{ $tc('swag-paypal-frw-credentials.textIntroPayPal') }} -

- {% endblock %} + - {% block sw_first_run_wizard_paypal_credentials_sandbox %} - + - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_button_container %} -
- - {% block sw_first_run_wizard_paypal_credentials_button %} - - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_indicator %} -
- -
- {% endblock %} -
- {% endblock %} + + {{ $tc('swag-paypal-frw-credentials.buttonGetSandboxCredentials') }} + - {% block sw_first_run_wizard_paypal_credentials_client_id %} - - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_client_secret %} - - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_merchant_id %} - - {% endblock %} - {% block sw_first_run_wizard_paypal_credentials_client_id_sandbox %} - - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_client_secret_sandbox %} - - {% endblock %} - - {% block sw_first_run_wizard_paypal_credentials_merchant_id_sandbox %} - - {% endblock %} - {% block sw_first_run_wizard_paypal_credentials_set_default %} - {% endblock %} - {% endblock %} + + +
{% endblock %} diff --git a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.scss b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.scss index 194384243..2f3fe6ca1 100644 --- a/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.scss +++ b/src/Resources/app/administration/src/module/extension/sw-first-run-wizard/sw-first-run-wizard-paypal-credentials/sw-first-run-wizard-paypal-credentials.scss @@ -3,29 +3,29 @@ .sw-first-run-wizard-paypal-credentials { width: 100%; - .sw-first-run-wizard-paypal-credentials__headerText { + display: flex; + flex-direction: column; + gap: 20px; + + &__headerText { font-weight: bold; color: $color-darkgray-200; - margin-bottom: 22px; } - .sw-first-run-wizard-paypal-credentials__button-container { - display: flex; - align-items: center; - margin-bottom: 22px; + &__as_default { + margin: 0; + } - .sw-first-run-wizard-paypal-credentials__indicator { - display: inline; - margin-left: 25px; + .sw-field.sw-block-field, .sw-field--switch { + margin: 0; + } - .sw-first-run-wizard-paypal-credentials__icon-successful { - color: $color-emerald-500; - margin-top: -5px; - } + .sw-field--switch .sw-field__label label { + padding-top: 0; + padding-bottom: 0; + } - .sw-first-run-wizard-paypal-credentials__text-indicator { - margin-left: 8px; - } - } + .swag-paypal-onboarding-button { + align-self: start; } } diff --git a/src/Resources/app/administration/src/module/extension/sw-settings-payment/components/swag-paypal-overview-card/index.ts b/src/Resources/app/administration/src/module/extension/sw-settings-payment/components/swag-paypal-overview-card/index.ts index eb019e3a2..fb0149b27 100644 --- a/src/Resources/app/administration/src/module/extension/sw-settings-payment/components/swag-paypal-overview-card/index.ts +++ b/src/Resources/app/administration/src/module/extension/sw-settings-payment/components/swag-paypal-overview-card/index.ts @@ -5,6 +5,9 @@ type ConfigComponent = { save:() => Promise<{ payPalWebhookErrors?: string[] }>; }; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-method-card` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-detail/index.ts b/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-detail/index.ts index f79b023ba..afcd17ea3 100644 --- a/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-detail/index.ts +++ b/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-detail/index.ts @@ -2,6 +2,9 @@ import type * as PayPal from 'src/types'; import template from './sw-settings-payment-detail.html.twig'; import './sw-settings-payment-detail.scss'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by swag-paypal-settings/extension/sw-settings-payment-detail + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-list/index.ts b/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-list/index.ts index fa36aac85..63da0a0c1 100644 --- a/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-list/index.ts +++ b/src/Resources/app/administration/src/module/extension/sw-settings-payment/sw-settings-payment-list/index.ts @@ -2,6 +2,9 @@ import type * as PayPal from 'src/types'; import template from './sw-settings-payment-list.html.twig'; import './sw-settings-payment-list.scss'; +/** + * @deprecated tag:v10.0.0 - Will be replaced without replacement + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/index.ts new file mode 100644 index 000000000..cb71cca1a --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/index.ts @@ -0,0 +1,47 @@ +import template from './swag-paypal-method-domain-association.html.twig'; +import './swag-paypal-method-domain-association.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + props: { + paymentMethod: { + type: Object as PropType>, + required: false, + default: null, + }, + }, + + data(): { + hidden: boolean; + } { + return { + hidden: localStorage.getItem('domain-association-hidden') === 'true', + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + domainAssociationLink() { + return this.settingsStore.get('SwagPayPal.settings.sandbox') + ? 'https://www.sandbox.paypal.com/uccservicing/apm/applepay' + : 'https://www.paypal.com/uccservicing/apm/applepay'; + }, + + show() { + return !this.hidden && this.paymentMethod?.active; + }, + }, + + methods: { + onCloseAlert() { + this.hidden = true; + localStorage.setItem('domain-association-hidden', 'true'); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.html.twig b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.html.twig new file mode 100644 index 000000000..3afb97fe1 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.html.twig @@ -0,0 +1,15 @@ + + {{ $t('swag-paypal-method.domainAssociation.title') }} + +
+ + + {{ $t('swag-paypal-method.domainAssociation.link') }} + +
diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.scss b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.scss new file mode 100644 index 000000000..2eb07197d --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.scss @@ -0,0 +1,8 @@ +@import "~scss/variables"; + +.swag-paypal-method-domain-association { + grid-column: span 4; + width: 100%; + margin-bottom: 4px; + font-size: $font-size-xs; +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.spec.ts new file mode 100644 index 000000000..116dd39c2 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-domain-association/swag-paypal-method-domain-association.spec.ts @@ -0,0 +1,86 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalMethodDomainAssociation from '.'; + +Shopware.Component.register('swag-paypal-method-domain-association', Promise.resolve(SwagPayPalMethodDomainAssociation)); + +async function createWrapper(active: boolean = true) { + return mount( + await Shopware.Component.build('swag-paypal-method-domain-association') as typeof SwagPayPalMethodDomainAssociation, + { + global: { + stubs: { + 'sw-alert': await wrapTestComponent('sw-alert', { sync: true }), + 'sw-alert-deprecated': await wrapTestComponent('sw-alert-deprecated', { sync: true }), + 'sw-external-link': await wrapTestComponent('sw-external-link', { sync: true }), + 'sw-external-link-deprecated': await wrapTestComponent('sw-external-link-deprecated', { sync: true }), + }, + }, + props: { + paymentMethod: { active } as TEntity<'payment_method'>, + }, + }, + ); +} + +describe('swag-paypal-method-domain-association', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should show', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.show).toBe(true); + + const alert = wrapper.findComponent('.sw-alert'); + expect(alert.exists()).toBe(true); + expect(alert.isVisible()).toBe(true); + }); + + it('should hide', async () => { + const wrapper = await createWrapper(false); + + const alert = wrapper.findComponent('.sw-alert'); + expect(alert.exists()).toBe(true); + + expect(wrapper.vm.show).toBe(false); + expect(alert.isVisible()).toBe(false); + }); + + it('should hide on close', async () => { + const wrapper = await createWrapper(); + + const alert = wrapper.findComponent('.sw-alert'); + expect(alert.exists()).toBe(true); + + expect(wrapper.vm.show).toBe(true); + expect(alert.isVisible()).toBe(true); + + alert.get('.sw-alert__close').trigger('click'); + await flushPromises(); + + expect(wrapper.vm.show).toBe(false); + expect(alert.isVisible()).toBe(false); + expect(localStorage.getItem('domain-association-hidden')).toBe('true'); + }); + + it('should link dependend on sandbox', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.settingsStore.setConfig(null, { 'SwagPayPal.settings.sandbox': true }); + expect(wrapper.vm.domainAssociationLink).toBe('https://www.sandbox.paypal.com/uccservicing/apm/applepay'); + + wrapper.vm.settingsStore.setConfig(null, { 'SwagPayPal.settings.sandbox': false }); + expect(wrapper.vm.domainAssociationLink).toBe('https://www.paypal.com/uccservicing/apm/applepay'); + }); + + it('should have external link', async () => { + const wrapper = await createWrapper(); + + const link = wrapper.findComponent('.sw-external-link'); + expect(link.exists()).toBe(true); + expect(link.attributes('href')).toBe(wrapper.vm.domainAssociationLink); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/index.ts new file mode 100644 index 000000000..b81cf7346 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/index.ts @@ -0,0 +1,49 @@ +import template from './swag-paypal-method-merchant-information.html.twig'; +import './swag-paypal-method-merchant-information.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + emits: ['save'], + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + merchantInformationStore() { + return Shopware.Store.get('swagPayPalMerchantInformation'); + }, + + merchantEmail() { + return this.merchantInformationStore.actual.merchantIntegrations?.primary_email + ?? this.merchantInformationStore.actual.merchantIntegrations?.tracking_id; + }, + + sandboxToggleDisabled(): boolean { + if (this.settingsStore.salesChannel) { + return false; + } + + if (this.settingsStore.isSandbox) { + return !!this.settingsStore.get('SwagPayPal.settings.clientSecretSandbox') && !this.settingsStore.get('SwagPayPal.settings.clientSecret'); + } + + return !!this.settingsStore.get('SwagPayPal.settings.clientSecret') && !this.settingsStore.get('SwagPayPal.settings.clientSecretSandbox'); + }, + + tooltipSandbox() { + return this.settingsStore.isSandbox + ? this.$t('swag-paypal-method.sandbox.onlySandboxTooltip') + : this.$t('swag-paypal-method.sandbox.onlyLiveTooltip'); + }, + }, + + methods: { + onSandboxToggle() { + this.$emit('save'); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.html.twig b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.html.twig new file mode 100644 index 000000000..28ac2dfe0 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.html.twig @@ -0,0 +1,23 @@ +
+ + + + +
+ +
+
diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.scss b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.scss new file mode 100644 index 000000000..8e29ad754 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.scss @@ -0,0 +1,29 @@ +@import "~scss/variables"; + +.swag-paypal-merchant-information { + background-color: $color-gray-50; + border: 1px solid $color-gray-300; + border-radius: $border-radius-default; + padding: 32px; + + display: grid; + grid-template-columns: 1fr auto; + grid-row-gap: 18px; + + &__email { + line-height: 24px; + font-weight: $font-weight-semi-bold; + font-size: $font-size-xs; + text-overflow: ellipsis; + text-wrap: nowrap; + overflow: hidden; + } + + &__buttons { + grid-column: span 2; + + display: flex; + gap: 12px; + align-items: center; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.spec.ts new file mode 100644 index 000000000..01b7a483b --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-method-merchant-information/swag-paypal-method-merchant-information.spec.ts @@ -0,0 +1,76 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalMethodMerchantInformation from '.'; +import MIFixture from 'SwagPayPal/app/store/merchant-information.fixture'; +import SettingsFixture from 'SwagPayPal/app/store/settings.fixture'; +import SwagPayPalSetting from 'SwagPayPal/app/component/swag-paypal-setting'; + +Shopware.Component.register('swag-paypal-setting', Promise.resolve(SwagPayPalSetting)); +Shopware.Component.register('swag-paypal-method-merchant-information', Promise.resolve(SwagPayPalMethodMerchantInformation)); + +async function createWrapper(active: boolean = true) { + return mount( + await Shopware.Component.build('swag-paypal-method-merchant-information') as typeof SwagPayPalMethodMerchantInformation, + { + global: { + stubs: { + 'sw-alert': await wrapTestComponent('sw-alert', { sync: true }), + 'sw-alert-deprecated': await wrapTestComponent('sw-alert-deprecated', { sync: true }), + 'sw-external-link': await wrapTestComponent('sw-external-link', { sync: true }), + 'sw-external-link-deprecated': await wrapTestComponent('sw-external-link-deprecated', { sync: true }), + 'swag-paypal-setting': await Shopware.Component.build('swag-paypal-setting'), + }, + }, + props: { + paymentMethod: { active } as TEntity<'payment_method'>, + }, + }, + ); +} + +describe('swag-paypal-method-merchant-information', () => { + beforeEach(() => { + Shopware.Store.get('swagPayPalMerchantInformation').set(null, MIFixture.Default); + Shopware.Store.get('swagPayPalSettings').setConfig(null, SettingsFixture.WithCredentials); + }); + + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should set disabled of sandbox toggle', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.merchantEmail).toBe('test@example.com'); + expect(wrapper.vm.sandboxToggleDisabled).toBe(true); + + wrapper.vm.settingsStore.setConfig(null, SettingsFixture.WithSandboxCredentials); + expect(wrapper.vm.sandboxToggleDisabled).toBe(false); + + wrapper.vm.settingsStore.set('SwagPayPal.settings.sandbox', true); + expect(wrapper.vm.sandboxToggleDisabled).toBe(true); + + wrapper.vm.settingsStore.setConfig(null, { + ...SettingsFixture.WithCredentials, + ...SettingsFixture.WithSandboxCredentials, + }); + + expect(wrapper.vm.sandboxToggleDisabled).toBe(false); + }); + + it('should trigger save on sandbox toggle', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.settingsStore.setConfig(null, { + ...SettingsFixture.WithCredentials, + ...SettingsFixture.WithSandboxCredentials, + }); + + const setting = wrapper.findComponent('.swag-paypal-setting'); + expect(setting.exists()).toBe(true); + setting.vm.$emit('update:value'); + + expect(wrapper.emitted('save')).toBeTruthy(); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/index.ts new file mode 100644 index 000000000..01746cca5 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/index.ts @@ -0,0 +1,104 @@ +import template from './swag-paypal-payment-method.html.twig'; +import './swag-paypal-payment-method.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + 'repositoryFactory', + ], + + emits: ['update:active'], + + props: { + paymentMethod: { + type: Object as PropType>, + required: true, + }, + }, + + computed: { + merchantInformationStore() { + return Shopware.Store.get('swagPayPalMerchantInformation'); + }, + + onboardingStatus() { + return this.merchantInformationStore.capabilities[this.paymentMethod.id]; + }, + + identifier(): string { + return this.paymentMethod.formattedHandlerIdentifier?.split('_').pop() ?? ''; + }, + + isPui() { + return this.identifier === 'puihandler'; + }, + + paymentMethodToggleDisabled() { + // should be able to deactivate active payment method + if (this.paymentMethod.active) { + return false; + } + + return !this.showEditLink; + }, + + showEditLink() { + return ['active', 'limited', 'mybank', 'test'].includes(this.onboardingStatus); + }, + + statusBadgeVariant() { + switch (this.onboardingStatus) { + case 'active': + return 'success'; + case 'limited': + case 'mybank': + return 'danger'; + case 'inactive': + case 'ineligible': + return 'neutral'; + case 'test': + case 'pending': + return 'info'; + default: + return 'neutral'; + } + }, + + onboardingStatusText() { + return this.$t(`swag-paypal-method.onboardingStatusText.${this.onboardingStatus}`); + }, + + onboardingStatusTooltip() { + const snippetKey = `swag-paypal-method.onboardingStatusTooltip.${this.onboardingStatus}`; + + if (!this.$te(snippetKey)) { + return null; + } + + return this.$t(snippetKey); + }, + + availabilityToolTip() { + const snippetKey = `swag-paypal-method.availabilityToolTip.${this.identifier}`; + + if (!this.$te(snippetKey)) { + return null; + } + + return this.$t(snippetKey); + }, + }, + + methods: { + onUpdateActive(active: boolean) { + // update:value is emitted twice + if (this.paymentMethod.active !== active) { + this.$emit('update:active', active); + } + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.html.twig b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.html.twig new file mode 100644 index 000000000..301ecb4ce --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.html.twig @@ -0,0 +1,62 @@ +
+ + +
+ {{ paymentMethod.translated.name }} + + ({{ $t('swag-paypal-method.ratePayLabel') }}) + +
+ + + +
+ + + + + + {{ onboardingStatusText }} + + + + {{ $t('swag-paypal-method.editDetail') }} + +
+ + + + +
diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.scss b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.scss new file mode 100644 index 000000000..21dbfb385 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.scss @@ -0,0 +1,63 @@ +@import "~scss/variables"; + +.swag-paypal-payment-method { + display: grid; + grid-template-columns: auto 1fr auto auto; + align-items: center; + border: 1px solid $color-gray-300; + border-radius: $border-radius-default; + gap: 16px 12px; + padding: 10px 16px; + font-size: $font-size-xs; + + &__name { + font-weight: $font-weight-semi-bold; + } + + &__icon { + width: 32px; + max-height: 24px; + } + + &__dynamic { + display: flex; + align-items: center; + justify-items: end; + gap: 12px; + + .sw-label { + margin: 0; + } + } + + &__status-label { + border: none; + font-weight: $font-weight-semi-bold; + + .sw-color-badge { + margin: 0; + } + + .sw-label__caption { + // sw-label will not center the text vertically + display: grid; + grid-template-columns: auto auto; + place-items: center; + height: 100%; + + gap: 4px; + + overflow-y: visible; // g's and p's are cut off + } + } + + & > .sw-skeleton-bar { + height: 24px; + margin: 0; + width: 150px; + } + + .sw-field--switch { + margin: 0; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.spec.ts new file mode 100644 index 000000000..b0012f40e --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/component/swag-paypal-payment-method/swag-paypal-payment-method.spec.ts @@ -0,0 +1,138 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalPaymentMethod from '.'; +import MIFixture from '../../../../app/store/merchant-information.fixture'; + +Shopware.Component.register('swag-paypal-payment-method', Promise.resolve(SwagPayPalPaymentMethod)); + +async function createWrapper( + paymentMethod: Partial> = {}, + availableTrans: string[] = [], +) { + return mount( + await Shopware.Component.build('swag-paypal-payment-method') as typeof SwagPayPalPaymentMethod, + { + global: { + stubs: { + 'sw-help-text': await wrapTestComponent('sw-help-text', { sync: true }), + 'sw-label': await wrapTestComponent('sw-label', { sync: true }), + 'sw-switch-field': await wrapTestComponent('sw-switch-field', { sync: true }), + 'sw-switch-field-deprecated': await wrapTestComponent('sw-switch-field-deprecated', { sync: true }), + 'sw-skeleton-bar': await wrapTestComponent('sw-skeleton-bar', { sync: true }), + 'sw-skeleton-bar-deprecated': await wrapTestComponent('sw-skeleton-bar-deprecated', { sync: true }), + }, + mocks: { + $te: (key: string) => availableTrans.includes(key), + }, + }, + props: { + paymentMethod: Shopware.Utils.object.merge({ + id: 'some-payment-method-id', + active: true, + translated: { name: 'PayPal' }, + formattedHandlerIdentifier: 'handler_swag_paypal', + }, paymentMethod) as TEntity<'payment_method'>, + }, + }, + ); +} + +describe('swag-paypal-payment-method', () => { + const store = Shopware.Store.get('swagPayPalMerchantInformation'); + + beforeEach(() => { + store.set(null, MIFixture.NotLoggedIn); + }); + + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should have correct state based on capability', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.onboardingStatus).toBe('inactive'); + expect(wrapper.vm.identifier).toBe('paypal'); + expect(wrapper.vm.isPui).toBe(false); + expect(wrapper.vm.paymentMethodToggleDisabled).toBe(false); + expect(wrapper.vm.showEditLink).toBe(false); + expect(wrapper.vm.statusBadgeVariant).toBe('neutral'); + expect(wrapper.vm.onboardingStatusText).toContain('onboardingStatusText.inactive'); + expect(wrapper.vm.onboardingStatusTooltip).toBeNull(); + expect(wrapper.vm.availabilityToolTip).toBeNull(); + + wrapper.vm.paymentMethod.active = false; + expect(wrapper.vm.paymentMethodToggleDisabled).toBe(true); + expect(wrapper.vm.showEditLink).toBe(false); + wrapper.vm.paymentMethod.active = true; + + store.set(null, MIFixture.Default); + + expect(wrapper.vm.onboardingStatus).toBe('active'); + expect(wrapper.vm.paymentMethodToggleDisabled).toBe(false); + expect(wrapper.vm.showEditLink).toBe(true); + expect(wrapper.vm.statusBadgeVariant).toBe('success'); + expect(wrapper.vm.onboardingStatusText).toContain('onboardingStatusText.active'); + expect(wrapper.vm.onboardingStatusTooltip).toBeNull(); + expect(wrapper.vm.availabilityToolTip).toBeNull(); + + wrapper.vm.paymentMethod.active = false; + expect(wrapper.vm.paymentMethodToggleDisabled).toBe(false); + expect(wrapper.vm.showEditLink).toBe(true); + wrapper.vm.paymentMethod.active = true; + }); + + it('should translate depending on snippet availability', async () => { + let wrapper = await createWrapper(); + + expect(wrapper.vm.onboardingStatusTooltip).toBeNull(); + expect(wrapper.vm.availabilityToolTip).toBeNull(); + + wrapper = await createWrapper({}, [ + 'swag-paypal-method.onboardingStatusTooltip.inactive', + ]); + + expect(wrapper.vm.onboardingStatusTooltip).toContain('onboardingStatusTooltip.inactive'); + expect(wrapper.vm.availabilityToolTip).toBeNull(); + + wrapper = await createWrapper({}, [ + 'swag-paypal-method.onboardingStatusTooltip.inactive', + 'swag-paypal-method.availabilityToolTip.paypal', + ]); + + expect(wrapper.vm.onboardingStatusTooltip).toContain('onboardingStatusTooltip.inactive'); + expect(wrapper.vm.availabilityToolTip).toContain('availabilityToolTip.paypal'); + }); + + it('should have correct loading state', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.find('.sw-skeleton-bar').exists()).toBe(false); + expect(wrapper.find('.swag-paypal-payment-method__dynamic').exists()).toBe(true); + + store.delete(null); + await wrapper.vm.$nextTick(); + + expect(wrapper.find('.sw-skeleton-bar').exists()).toBe(true); + expect(wrapper.find('.swag-paypal-payment-method__dynamic').exists()).toBe(false); + }); + + it('should toggle payment method active state', async () => { + const wrapper = await createWrapper({ active: true }); + + const switchField = wrapper.findComponent('.sw-field--switch'); + expect(switchField.exists()).toBe(true); + + // emitting same value should not trigger event + // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- component exists, we can emit + await switchField.vm.$emit('update:value', true); + + expect(wrapper.emitted('update:active')).toBeFalsy(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- component exists, we can emit + await switchField.vm.$emit('update:value', false); + + expect(wrapper.emitted('update:active')).toBeTruthy(); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/index.ts new file mode 100644 index 000000000..0fe3c689d --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/index.ts @@ -0,0 +1,46 @@ +import type * as PayPal from 'src/types'; +import template from './sw-settings-payment-detail.html.twig'; +import './sw-settings-payment-detail.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + inject: [ + 'SwagPayPalSettingsService', + ], + + data(): { + capabilities: PayPal.Setting<'merchant_information'>['capabilities']; + } { + return { + capabilities: {}, + }; + }, + + computed: { + needsOnboarding(): boolean { + // @ts-expect-error - paymentMethod is from extended component + if (!this.paymentMethod || !this.capabilities) { + return true; + } + + // @ts-expect-error - paymentMethod is from extended component + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return this.capabilities[this.paymentMethod.id] === 'inactive'; + }, + }, + + methods: { + createdComponent() { + this.$super('createdComponent'); + + this.fetchMerchantCapabilities(); + }, + + async fetchMerchantCapabilities() { + const merchantInformation = await this.SwagPayPalSettingsService.getMerchantInformation(); + this.capabilities = merchantInformation.capabilities ?? {}; + }, + }, +}); + diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.html.twig b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.html.twig new file mode 100644 index 000000000..d8fbf15d5 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.html.twig @@ -0,0 +1,30 @@ +{% block sw_settings_payment_detail_content_field_plugin %} + + {{ $t('sw-plugin-box.buttonPluginSettings') }} + + + +{% endblock %} + +{% block sw_settings_payment_detail_content_field_active %} + + + +{% endblock %} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.scss b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.scss new file mode 100644 index 000000000..6058b9cd5 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/extension/sw-settings-payment/sw-settings-payment-detail/sw-settings-payment-detail.scss @@ -0,0 +1,5 @@ +@import "~scss/variables"; + +.swag-paypal-go-to-settings-link { + color: $color-shopware-brand-500; +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/index.ts new file mode 100644 index 000000000..96cbd09f0 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/index.ts @@ -0,0 +1,36 @@ +import { ui } from '@shopware-ag/meteor-admin-sdk'; + +Shopware.Component.register('swag-paypal-method-domain-association', () => import('./component/swag-paypal-method-domain-association')); +Shopware.Component.register('swag-paypal-method-merchant-information', () => import('./component/swag-paypal-method-merchant-information')); +Shopware.Component.register('swag-paypal-payment-method', () => import('./component/swag-paypal-payment-method')); + +Shopware.Component.register('swag-paypal-method-card', () => import('./view/swag-paypal-method-card')); + +Shopware.Component.override('sw-settings-payment-detail', () => import('./extension/sw-settings-payment/sw-settings-payment-detail')); + +ui.module.payment.overviewCard.add({ + positionId: 'swag-paypal-method-card-before', + component: 'swag-paypal-method-card', + paymentMethodHandlers: [ + 'handler_swag_trustlyapmhandler', + 'handler_swag_sofortapmhandler', + 'handler_swag_p24apmhandler', + 'handler_swag_oxxoapmhandler', + 'handler_swag_mybankapmhandler', + 'handler_swag_multibancoapmhandler', + 'handler_swag_idealapmhandler', + 'handler_swag_giropayapmhandler', + 'handler_swag_epsapmhandler', + 'handler_swag_blikapmhandler', + 'handler_swag_bancontactapmhandler', + 'handler_swag_sepahandler', + 'handler_swag_acdchandler', + 'handler_swag_puihandler', + 'handler_swag_paypalpaymenthandler', + 'handler_swag_pospayment', + 'handler_swag_venmohandler', + 'handler_swag_paylaterhandler', + 'handler_swag_applepayhandler', + 'handler_swag_googlepayhandler', + ], +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/snippet/de-DE.json b/src/Resources/app/administration/src/module/swag-paypal-method/snippet/de-DE.json new file mode 100644 index 000000000..a75723946 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/snippet/de-DE.json @@ -0,0 +1,64 @@ +{ + "swag-paypal-method": { + "cardTitle": "PayPal Checkout", + "settingsLink": "Einstellungen", + "header": "Mehr Zahlungsmöglichkeiten in einer Lösung von PayPal", + "description": "PayPal Checkout ist die neue All-in-One-Lösung, mit der Sie leistungsstarke und flexible Funktionen zur Zahlungsabwicklung auf Marktplätzen und anderen Handelsplattformen anbieten können.", + "banner": "Mit PayPal Checkout Kannst Du Deinen Kunden die Bezahlung per Rechnung, Kreditkarte und viele andere lokale Zahlungsmethoden anbieten. PayPal Checkout unterstützt Dich mit der neuesten Technologie und bringt Dir höchste Flexibilität. Du behältst Deine bisherigen Zahlungsarten und die Gebühren bleiben gleich! Aktiviere PayPal Checkout und deaktiviere PayPal PLUS, um doppelte Zahlungsmethoden in Deinem Shop zu vermeiden.", + "bannerLink": "Mehr Informationen", + "sandbox": { + "onlySandboxTooltip": "Du bist bisher nur mit einem PayPal-Sandbox-Konto verbunden.", + "onlyLiveTooltip": "Du bist bisher nur mit einem PayPal-Live-Konto verbunden." + }, + "switch": { + "label": "Aktiv", + "active": "Zahlungsart \"{name}\" ist jetzt aktiv.", + "inactive": "Zahlungsart \"{name}\" ist jetzt inaktiv." + }, + "domainAssociation": { + "title": "Bitte vergewissere dich, dass die Domain korrekt eingestellt ist, damit Apple Pay funktioniert. Die Domain-Zuordnungsdatei ist bereits hinterlegt.", + "link": "Domain-Einstellungen öffnen" + }, + "paymentMethodText": "Zahlungsmöglichkeiten", + "appImageAlt": "PayPal", + "ratePayLabel": "von Ratepay", + "editDetail": "Details bearbeiten", + "availabilityToolTip": { + "bancontactapmhandler": "Bancontact ist für die folgenden Länder verfügbar: Belgien", + "blikapmhandler": "BLIK ist für die folgenden Länder verfügbar: Polen", + "boletobancarioapmhandler": "Boleto Bancário ist für die folgenden Länder verfügbar: Brasilien", + "epsapmhandler": "eps ist für die folgenden Länder verfügbar: Österreich", + "idealapmhandler": "iDEAL ist für die folgenden Länder verfügbar: Niederlande", + "multibancoapmhandler": "Multibanco ist für die folgenden Länder verfügbar: Portugal", + "mybankapmhandler": "MyBank ist für die folgenden Länder verfügbar: Italien", + "oxxoapmhandler": "OXXO ist für die folgenden Länder verfügbar: Mexiko", + "p24apmhandler": "Przelewy24 ist für die folgenden Länder verfügbar: Polen", + "paylaterhandler": "Später Bezahlen ist für die folgenden Länder verfügbar: Australien, Deutschland, Frankreich, Italien, Spanien, Vereinigte Staaten, Vereinigtes Königreich", + "puihandler": "Rechnungskauf ist für die folgenden Länder verfügbar: Deutschland", + "sepahandler": "SEPA Lastschrift ist für die folgenden Länder verfügbar: Deutschland", + "venmohandler": "Venmo ist für die folgenden Länder verfügbar: USA", + "trustlyapmhandler": "Trustly ist für die folgenden Länder verfügbar: Estland, Finnland, Niederlande, Schweden" + }, + "onboardingStatusText": { + "active": "Autorisiert", + "limited": "Limitiert", + "pending": "Authorisierung ausstehend", + "ineligible": "Gesperrt", + "inactive": "Onboarding benötigt", + "mybank": "Limitiert", + "test": "Test Modus" + }, + "onboardingStatusTooltip": { + "ineligible": "PayPal hat uns informiert, dass diese Zahlungsmethode aktuell für Ihren Account nicht freigeschaltet ist.", + "limited": "PayPal hat uns informiert, dass Einschränkugen dieser Zahlungsart für Ihren Account bestehen.", + "mybank": "Händler, die MyBank nach Februar 2023 aktivieren, benötigen eine manuelle Genehmigung von PayPal. Wenden Sie sich an den PayPal-Support, um weitere Informationen zu erhalten." + }, + "merchantStatusText": { + "emailUnconfirmed": "E-Mail unbestätigt", + "paymentsUnreceivable": "Zahlungen nicht empfangbar", + "onboardingNeeded": "Erneutes Onboarding benötigt", + "connected": "Verbunden", + "notConnected": "Nicht verbunden" + } + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/snippet/en-GB.json b/src/Resources/app/administration/src/module/swag-paypal-method/snippet/en-GB.json new file mode 100644 index 000000000..774f1eedb --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/snippet/en-GB.json @@ -0,0 +1,66 @@ +{ + "swag-paypal-method": { + "cardTitle": "PayPal Checkout", + "settingsLink": "Settings", + "header": "More payment methods with one solution from PayPal", + "description": "PayPal Checkout is the new all-in-one solution that lets you offer powerful flexibile payment processing features on the marketplaces and other commerce platforms.", + "banner": "With PayPal Checkout, you can offer your customers payment by invoice, credit card and other local payment methods. PayPal Checkout supports you with the latest technology and brings you the highest flexibility. You keep your existing payment methods, and the fees remain the same! Activate PayPal Checkout and deactivate PayPal PLUS to avoid duplicate payment methods in your shop.", + "bannerLink": "More information", + "sandbox": { + "label": "Sandbox", + "onlySandboxTooltip": "You are only connected a sandbox PayPal account so far.", + "onlyLiveTooltip": "You are only connected a live PayPal account so far.", + "helpText": "Enable, if you want to test the PayPal integration." + }, + "switch": { + "label": "Active", + "active": "Payment method \"{name}\" is now active.", + "inactive": "Payment method \"{name}\" is now inactive." + }, + "domainAssociation": { + "title": "Please make sure your domain is set correctly for Apple Pay to work properly. The domain association file is already present.", + "link": "Open domains settings" + }, + "paymentMethodText": "Payment methods", + "appImageAlt": "PayPal", + "ratePayLabel": "by Ratepay", + "editDetail": "Edit details", + "availabilityToolTip": { + "bancontactapmhandler": "Bancontact is available for the following countries: Belgium", + "blikapmhandler": "BLIK is available for the following countries: Poland", + "boletobancarioapmhandler": "Boleto Bancário is available for the following countries: Brazil", + "epsapmhandler": "eps is available for the following countries: Austria", + "idealapmhandler": "iDEAL is available for the following countries: Netherlands", + "multibancoapmhandler": "Multibanco is available for the following countries: Portugal", + "mybankapmhandler": "MyBank is available for the following countries: Italy", + "oxxoapmhandler": "OXXO is available for the following countries: Mexico", + "p24apmhandler": "Przelewy24 is available for the following countries: Poland", + "paylaterhandler": "Pay Later is available for the following countries: Australia, France, Germany, Italy, Spain, United Kingdom, United States", + "puihandler": "Pay upon invoice is available for the following countries: Germany", + "sepahandler": "SEPA Lastschrift is available for the following countries: Germany", + "venmohandler": "Venmo is available for the following countries: United States", + "trustlyapmhandler": "Trustly is available for the following countries: Estonia, Finland, Netherlands, Sweden" + }, + "onboardingStatusText": { + "active": "Authorized", + "limited": "Limited", + "pending": "Authorization pending", + "ineligible": "Ineligible", + "inactive": "Onboarding needed", + "mybank": "Limited", + "test": "Test mode" + }, + "onboardingStatusTooltip": { + "ineligible": "PayPal informed us, that this payment method is currently ineligible for your account.", + "limited": "PayPal informed us, that this payment method has some limitations for your account.", + "mybank": "Merchants enabling MyBank after February 2023 will need manual approval by PayPal. Reach out to PayPal support for further information on this." + }, + "merchantStatusText": { + "emailUnconfirmed": "Email unconfirmed", + "paymentsUnreceivable": "Payments unreceivable", + "onboardingNeeded": "Re-onboarding needed", + "connected": "Connected", + "notConnected": "Not connected" + } + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/index.ts b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/index.ts new file mode 100644 index 000000000..8badcabf5 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/index.ts @@ -0,0 +1,132 @@ +import template from './swag-paypal-method-card.html.twig'; +import './swag-paypal-method-card.scss'; + +const { Context } = Shopware; +const { Criteria } = Shopware.Data; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'repositoryFactory', + ], + + mixins: [ + Shopware.Mixin.getByName('notification'), + Shopware.Mixin.getByName('swag-paypal-settings'), + Shopware.Mixin.getByName('swag-paypal-merchant-information'), + ], + + data(): { + isLoadingPaymentMethods: boolean; + paymentMethods: TEntity<'payment_method'>[]; + } { + return { + isLoadingPaymentMethods: true, + paymentMethods: [], + }; + }, + + computed: { + assetFilter() { + return Shopware.Filter.getByName('asset'); + }, + + paymentMethodRepository(): TRepository<'payment_method'> { + return this.repositoryFactory.create('payment_method'); + }, + + paymentMethodCriteria(): TCriteria { + return (new Criteria(1, 500)) + .addAssociation('media') + .addFilter(Criteria.equals('plugin.name', 'SwagPayPal')) + .addSorting(Criteria.sort('position', 'ASC')); + }, + + merchantStatus() { + const merchantIntegrations = this.merchantInformationStore.actual.merchantIntegrations; + + if (!merchantIntegrations) { + return 'notConnected'; + } else if (!this.merchantInformationStore.canPPCP) { + return 'onboardingNeeded'; + } else if (!merchantIntegrations?.primary_email_confirmed) { + return 'emailUnconfirmed'; + } else if (!merchantIntegrations?.payments_receivable) { + return 'paymentsUnreceivable'; + } else { + return 'connected'; + } + }, + + statusVariant() { + switch (this.merchantStatus) { + case 'onboardingNeeded': + case 'notConnected': + return 'danger'; + case 'emailUnconfirmed': + case 'paymentsUnreceivable': + return 'warning'; + case 'connected': + return 'success'; + } + }, + + statusText() { + return this.$t(`swag-paypal-method.merchantStatusText.${this.merchantStatus}`); + }, + + showMerchantInformation() { + return this.merchantInformationStore.canPPCP; + }, + }, + + created() { + this.createdComponent(); + }, + + methods: { + createdComponent() { + this.fetchPaymentMethods(); + }, + + async fetchPaymentMethods() { + this.isLoadingPaymentMethods = true; + + const paymentMethods = await this.paymentMethodRepository.search(this.paymentMethodCriteria, Context.api) + .catch(() => ([])); + + this.paymentMethods = paymentMethods + .filter((pm) => { + if (pm.formattedHandlerIdentifier === 'handler_swag_pospayment') { + return false; + } + + return !([ + 'handler_swag_trustlyapmhandler', + 'handler_swag_giropayapmhandler', + 'handler_swag_sofortapmhandler', + ].includes(pm.formattedHandlerIdentifier ?? '') && !pm.active); + }); + + this.isLoadingPaymentMethods = false; + }, + + async onUpdateActive(paymentMethod: TEntity<'payment_method'>, active: boolean) { + paymentMethod.active = active; + + await this.paymentMethodRepository.save(paymentMethod, Context.api) + .then(() => { + this.createNotificationSuccess({ + message: this.$t( + `swag-paypal-method.switch.${paymentMethod.active ? 'active' : 'inactive'}`, + { name: paymentMethod.translated?.name || paymentMethod.name }, + ), + }); + }) + .catch(() => { paymentMethod.active = !active; }); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.html.twig b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.html.twig new file mode 100644 index 000000000..40bc7579a --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.html.twig @@ -0,0 +1,110 @@ + + + + + + + +
+ + + + {% block swag_paypal_method_card_onboarding_buttons_connected %} + + + + {% endblock %} + + + + + {% block swag_paypal_method_card_payment_methods %} +

+ {{ $t('swag-paypal-method.paymentMethodText') }} +

+ + + +
+ +
+ {% endblock %} +
+
diff --git a/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.scss b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.scss new file mode 100644 index 000000000..f0beb1cfb --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-method/view/swag-paypal-method-card/swag-paypal-method-card.scss @@ -0,0 +1,56 @@ +@import "~scss/variables"; + +.swag-paypal-method-card { + .sw-card__titles { + display: flex; + gap: 16px; + align-items: center; + } + + &__status { + margin: 0; + + .sw-color-badge { + margin: 0; + } + } + + &__content { + display: flex; + flex-direction: column; + gap: 16px; + padding: 30px; + } + + &__header { + font-weight: $font-weight-bold; + font-size: $font-size-m; + line-height: 42px; + } + + &__description { + line-height: 25px; + } + + &__onboarding_buttons { + display: flex; + gap: 12px; + align-items: center; + } + + &__payment-method-headline { + font-size: $font-size-m; + margin: 16px 0 8px; + } + + &__listing { + display: flex; + gap: 16px; + flex-direction: column; + } + + & .sw-skeleton-bar { + height: 24px; + margin: 0; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/index.ts new file mode 100644 index 000000000..2038e8487 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/index.ts @@ -0,0 +1,8 @@ +import template from './swag-paypal-settings-icon.html.twig'; +import './swag-paypal-settings-icon.scss'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.html.twig new file mode 100644 index 000000000..de8d3cd01 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.html.twig @@ -0,0 +1 @@ + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.scss b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.scss new file mode 100644 index 000000000..311281b5a --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-icon/swag-paypal-settings-icon.scss @@ -0,0 +1,15 @@ +@import "~scss/mixins"; + +.swag-paypal-settings-icon { + @include size(24px); + + display: inline-block; + vertical-align: middle; + line-height: 0; + + > svg { + width: 100%; + height: 100%; + vertical-align: middle; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/index.ts new file mode 100644 index 000000000..ad4ff636d --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/index.ts @@ -0,0 +1,65 @@ +import template from './swag-paypal-settings-locale-select.html.twig'; +import './swag-paypal-settings-locale-select.scss'; +import { LOCALES, type LOCALE } from '../../../../constant/swag-paypal-settings.constant'; + +type LocaleOption = { + value: string | null; + dashed: string | null; + label: string; +}; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + props: { + value: { + type: String as PropType, + required: false, + default: null, + }, + }, + + computed: { + options(): LocaleOption[] { + const options = LOCALES.map((locale) => this.toOption(locale)); + + options.splice(0, 0, { + value: null, + dashed: null, + label: this.$t('swag-paypal-settings-locale-select.automatic'), + }); + + if (this.invalidError) { + options.splice(0, 0, this.toOption(this.value)); + } + + return options; + }, + + invalidError() { + if (this.value && !LOCALES.includes(this.value)) { + return { detail: this.$t('swag-paypal-settings-locale-select.invalid') }; + } + + return undefined; + }, + }, + + methods: { + toOption(locale: string): LocaleOption { + const localeDash = locale.replace('_', '-'); + + return { + value: locale, + dashed: localeDash, + label: this.$te(`locale.${localeDash}`) ? this.$t(`locale.${localeDash}`) : localeDash, + }; + }, + + updateValue(value: unknown) { + this.$emit('update:value', value); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.html.twig new file mode 100644 index 000000000..815a3ef1d --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.html.twig @@ -0,0 +1,29 @@ + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.scss b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.scss new file mode 100644 index 000000000..6f923c693 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.scss @@ -0,0 +1,13 @@ +@import "~scss/variables"; + +.swag-paypal-settings-locale-select { + &__locale_info { + .sw-highlight-text, .sw-select-result__result-item-text { + display: inline; + } + } + + &__locale { + color: $color-gray-600; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.spec.ts new file mode 100644 index 000000000..2df75f35b --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-locale-select/swag-paypal-settings-locale-select.spec.ts @@ -0,0 +1,71 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsLocaleSelect from '.'; +import { LOCALES, type LOCALE } from 'SwagPayPal/constant/swag-paypal-settings.constant'; + +Shopware.Component.register('swag-paypal-settings-locale-select', Promise.resolve(SwagPayPalSettingsLocaleSelect)); + +async function createWrapper(value: LOCALE | undefined = undefined) { + return mount( + await Shopware.Component.build('swag-paypal-settings-locale-select') as typeof SwagPayPalSettingsLocaleSelect, + { + props: { value }, + global: { + mocks: { + $t: (key: string) => key, + $te: () => true, + }, + stubs: { + 'sw-single-select': await wrapTestComponent('sw-single-select', { sync: true }), + }, + }, + }, + ); +} + +describe('swag-paypal-settings-locale-select', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should update value', async () => { + const wrapper = await createWrapper(); + + const select = wrapper.findComponent('.sw-single-select'); + expect(select.exists()).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + select.vm.$emit('update:value', 'de-DE'); + + expect(wrapper.emitted('update:value')).toEqual([['de-DE']]); + }); + + it('should convert locale to option', async () => { + const wrapper = await createWrapper(); + + const option = wrapper.vm.toOption('de_DE'); + + expect(option).toEqual({ + value: 'de_DE', + dashed: 'de-DE', + label: 'locale.de-DE', + }); + }); + + it('should have options', async () => { + const wrapper = await createWrapper(); + + const values = wrapper.vm.options.map((option) => option.value); + + expect(values).toEqual([null, ...LOCALES]); + }); + + it('should have invalid error', async () => { + const wrapper = await createWrapper('invalid-locale' as LOCALE); + + expect(wrapper.vm.invalidError).toEqual({ detail: 'swag-paypal-settings-locale-select.invalid' }); + + const values = wrapper.vm.options.map((option) => option.value); + expect(values).toEqual(['invalid-locale', null, ...LOCALES]); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/index.ts new file mode 100644 index 000000000..436df54c3 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/index.ts @@ -0,0 +1,95 @@ +import template from './swag-paypal-settings-sales-channel-switch.html.twig'; + +const { Defaults } = Shopware; +const { Criteria } = Shopware.Data; + +type SalesChannel = { + value: string | null; + label: string; +}; + +/** + * @private + */ +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + 'repositoryFactory', + 'SwagPaypalPaymentMethodService', + ], + + data(): { + isLoading: boolean; + salesChannels: SalesChannel[]; + defaultPaymentMethods: 'none' | 'loading' | 'success'; + } { + return { + isLoading: true, + salesChannels: [], + defaultPaymentMethods: 'none', + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + salesChannelRepository() { + return this.repositoryFactory.create('sales_channel'); + }, + + salesChannelCriteria(): TCriteria { + const criteria = new Criteria(1, 500); + + criteria.addFilter(Criteria.equalsAny('typeId', [ + Defaults.storefrontSalesChannelTypeId, + Defaults.apiSalesChannelTypeId, + ])); + + return criteria; + }, + }, + + created() { + this.createdComponent(); + }, + + methods: { + createdComponent() { + this.fetchSalesChannels(); + }, + + async fetchSalesChannels() { + try { + const salesChannels = await this.salesChannelRepository.search(this.salesChannelCriteria, Shopware.Context.api); + + this.salesChannels = [{ + value: null, + label: this.$t('sw-sales-channel-switch.labelDefaultOption'), + }]; + + salesChannels.forEach((salesChannel) => { + this.salesChannels.push({ + value: salesChannel.id, + label: salesChannel.translated?.name || salesChannel.name, + }); + }); + } finally { + this.isLoading = false; + } + }, + + onSetPaymentMethodDefault() { + this.defaultPaymentMethods = 'loading'; + + this.SwagPaypalPaymentMethodService.setDefaultPaymentForSalesChannel(this.settingsStore.salesChannel) + .then(() => { this.defaultPaymentMethods = 'success'; }) + .catch(() => { this.defaultPaymentMethods = 'none'; }); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.html.twig new file mode 100644 index 000000000..e7abdf033 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.html.twig @@ -0,0 +1,34 @@ + + + + + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.spec.ts new file mode 100644 index 000000000..b78015536 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-sales-channel-switch/swag-paypal-settings-sales-channel-switch.spec.ts @@ -0,0 +1,94 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsSalesChannelSwitch from '.'; + +Shopware.Component.register('swag-paypal-settings-sales-channel-switch', Promise.resolve(SwagPayPalSettingsSalesChannelSwitch)); + +async function createWrapper() { + return mount( + await Shopware.Component.build('swag-paypal-settings-sales-channel-switch') as typeof SwagPayPalSettingsSalesChannelSwitch, + { + global: { + mocks: { $t: (key: string) => key }, + provide: { + acl: { can: () => true }, + repositoryFactory: { + create: () => ({ + search: jest.fn(() => Promise.resolve([ + { id: 'id-1', name: 'Name 1' }, + { id: 'id-2', name: 'Name 2' }, + ])), + }), + }, + SwagPaypalPaymentMethodService: { + setDefaultPaymentForSalesChannel: jest.fn(() => Promise.resolve()), + }, + }, + stubs: { + 'sw-card': await wrapTestComponent('sw-card', { sync: true }), + 'sw-card-deprecated': await wrapTestComponent('sw-card-deprecated', { sync: true }), + 'sw-container': await wrapTestComponent('sw-container', { sync: true }), + 'sw-internal-link': await wrapTestComponent('sw-internal-link', { sync: true }), + 'sw-button-process': await wrapTestComponent('sw-button-process', { sync: true }), + 'sw-single-select': await wrapTestComponent('sw-single-select', { sync: true }), + }, + }, + }, + ); +} + +describe('swag-paypal-settings-sales-channel-switch', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should fetch sales channels on creation', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.salesChannelRepository.search).toHaveBeenCalledWith( + wrapper.vm.salesChannelCriteria, + Shopware.Context.api, + ); + expect(wrapper.vm.salesChannels).toEqual([ + { value: null, label: 'sw-sales-channel-switch.labelDefaultOption' }, + { value: 'id-1', label: 'Name 1' }, + { value: 'id-2', label: 'Name 2' }, + ]); + }); + + it('should select a different sales channel', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.settingsStore.salesChannel).toBeNull(); + + const select = wrapper.findComponent('.sw-single-select'); + expect(select.exists()).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + select.vm.$emit('update:value', 'id-2'); + + expect(wrapper.vm.settingsStore.salesChannel).toBe('id-2'); + }); + + it('should set payment method as default', async () => { + const wrapper = await createWrapper(); + + const button = wrapper.findComponent('.sw-button-process'); + expect(button.exists()).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + button.vm.$emit('click'); + + expect(wrapper.vm.defaultPaymentMethods).toBe('loading'); + expect(wrapper.vm.SwagPaypalPaymentMethodService.setDefaultPaymentForSalesChannel).toHaveBeenCalledWith(null); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.defaultPaymentMethods).toBe('success'); + }); + + it('should have link to payment method', async () => { + const wrapper = await createWrapper(); + + const link = wrapper.findComponent('.sw-internal-link'); + expect(link.exists()).toBe(true); + expect(link.vm.routerLink).toEqual({ name: 'sw.settings.payment.overview' }); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/index.ts new file mode 100644 index 000000000..8ce47d538 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/index.ts @@ -0,0 +1,119 @@ +import type * as PayPal from 'src/types'; +import template from './swag-paypal-settings-webhook.html.twig'; +import './swag-paypal-settings-webhook.scss'; + +const STATUS_WEBHOOK_MISSING = 'missing'; +const STATUS_WEBHOOK_INVALID = 'invalid'; +const STATUS_WEBHOOK_VALID = 'valid'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + 'SwagPayPalWebhookService', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-notification'), + ], + + data(): { + allWebhookStatus: Record; + status: 'none' | 'fetching' | 'refreshing'; + } { + return { + allWebhookStatus: {}, + status: 'none', + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + webhookStatus(): string | undefined { + return this.allWebhookStatus[String(this.settingsStore.salesChannel)]; + }, + + webhookStatusLabel() { + return this.$t(`swag-paypal-settings.webhook.status.${this.webhookStatus || 'unknown'}`); + }, + + webhookStatusVariant(): 'danger' | 'warning' | 'success' | 'neutral' { + switch (this.webhookStatus) { + case STATUS_WEBHOOK_MISSING: + return 'danger'; + + case STATUS_WEBHOOK_INVALID: + return 'warning'; + + case STATUS_WEBHOOK_VALID: + return 'success'; + + default: + return 'neutral'; + } + }, + + allowRefresh(): boolean { + return [STATUS_WEBHOOK_INVALID, STATUS_WEBHOOK_MISSING] + .includes(this.webhookStatus ?? ''); + }, + }, + + watch: { + 'settingsStore.salesChannel': { + immediate: true, + handler() { + this.fetchWebhookStatus(this.settingsStore.salesChannel); + }, + }, + }, + + methods: { + fetchWebhookStatus(salesChannelId: string | null) { + if (this.webhookStatus) { + return; + } + + this.status = 'fetching'; + + this.SwagPayPalWebhookService.status(salesChannelId) + .then((response) => { + this.allWebhookStatus[String(salesChannelId)] = response.result; + this.status = 'none'; + }) + .catch((errorResponse: PayPal.ServiceError) => { + this.createNotificationError({ + title: this.$t('swag-paypal.notifications.webhook.title'), + message: this.$t('swag-paypal.notifications.webhook.errorMessage', { + message: this.createMessageFromError(errorResponse), + }), + }); + }); + }, + + async onRefreshWebhook() { + this.status = 'refreshing'; + + await this.SwagPayPalWebhookService + .register(this.settingsStore.salesChannel) + .catch((errorResponse: PayPal.ServiceError) => { + this.createNotificationError({ + title: this.$t('swag-paypal.notifications.webhook.title'), + message: this.$t('swag-paypal.notifications.webhook.errorMessage', { + message: this.createMessageFromError(errorResponse), + }), + }); + }); + + this.status = 'none'; + + this.fetchWebhookStatus(this.settingsStore.salesChannel); + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.html.twig new file mode 100644 index 000000000..2704e4d64 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.html.twig @@ -0,0 +1,35 @@ + + + + + {{ $t('swag-paypal-settings.webhook.info') }} + + + + {{ $t('swag-paypal-settings.webhook.buttonRefresh') }} + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.scss b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.scss new file mode 100644 index 000000000..a38ad8a79 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.scss @@ -0,0 +1,31 @@ +@import "~scss/variables"; + +.swag-paypal-settings-webhook { + .sw-card__titles { + display: grid; + grid-template-columns: min-content min-content; + gap: 12px; + place-items: center; + } + + .sw-card__content { + display: grid; + gap: 16px; + place-items: end; + font-size: $font-size-xs; + line-height: $line-height-sm; + } + + &__label { + border: 0; + margin: 0; + + .sw-label__caption { + overflow: visible; + } + + .sw-color-badge { + margin: 0 0 0 2px; + } + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.spec.ts new file mode 100644 index 000000000..93c0f0e66 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/components/swag-paypal-settings-webhook/swag-paypal-settings-webhook.spec.ts @@ -0,0 +1,151 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsWebhook from '.'; + +Shopware.Component.register('swag-paypal-settings-webhook', Promise.resolve(SwagPayPalSettingsWebhook)); + +async function createWrapper() { + return mount( + await Shopware.Component.build('swag-paypal-settings-webhook') as typeof SwagPayPalSettingsWebhook, + { + global: { + mocks: { $tc: (key: string) => key }, + provide: { + acl: { + can: () => true, + }, + SwagPayPalWebhookService: { + status: jest.fn(() => Promise.resolve({ result: null })), + register: jest.fn(() => Promise.resolve()), + }, + }, + stubs: { + 'sw-button': await wrapTestComponent('sw-button', { sync: true }), + 'sw-card': await wrapTestComponent('sw-card', { sync: true }), + 'sw-card-deprecated': await wrapTestComponent('sw-card-deprecated', { sync: true }), + 'sw-label': await wrapTestComponent('sw-label', { sync: true }), + }, + }, + }, + ); +} + +describe('swag-paypal-settings-webhook', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should fetch status on creation', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm.SwagPayPalWebhookService.status).toBeCalled(); + }); + + it('should pick correct status variant', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.allWebhookStatus.null = 'valid'; + expect(wrapper.vm.webhookStatusVariant).toBe('success'); + + wrapper.vm.allWebhookStatus.null = 'missing'; + expect(wrapper.vm.webhookStatusVariant).toBe('danger'); + + wrapper.vm.allWebhookStatus.null = 'invalid'; + expect(wrapper.vm.webhookStatusVariant).toBe('warning'); + + wrapper.vm.allWebhookStatus.null = ''; + expect(wrapper.vm.webhookStatusVariant).toBe('neutral'); + + wrapper.vm.allWebhookStatus.null = undefined; + expect(wrapper.vm.webhookStatusVariant).toBe('neutral'); + }); + + it('should allow refresh', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.allWebhookStatus.null = 'valid'; + expect(wrapper.vm.allowRefresh).toBe(false); + + wrapper.vm.allWebhookStatus.null = 'missing'; + expect(wrapper.vm.allowRefresh).toBe(true); + + wrapper.vm.allWebhookStatus.null = 'invalid'; + expect(wrapper.vm.allowRefresh).toBe(true); + + wrapper.vm.allWebhookStatus.null = ''; + expect(wrapper.vm.allowRefresh).toBe(false); + + wrapper.vm.allWebhookStatus.null = undefined; + expect(wrapper.vm.allowRefresh).toBe(false); + }); + + it('should have correct status label', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.allWebhookStatus.null = 'valid'; + expect(wrapper.vm.webhookStatusLabel).toBe('swag-paypal-settings.webhook.status.valid'); + + wrapper.vm.allWebhookStatus.null = 'missing'; + expect(wrapper.vm.webhookStatusLabel).toBe('swag-paypal-settings.webhook.status.missing'); + + wrapper.vm.allWebhookStatus.null = 'invalid'; + expect(wrapper.vm.webhookStatusLabel).toBe('swag-paypal-settings.webhook.status.invalid'); + + wrapper.vm.allWebhookStatus.null = ''; + expect(wrapper.vm.webhookStatusLabel).toBe('swag-paypal-settings.webhook.status.unknown'); + + wrapper.vm.allWebhookStatus.null = undefined; + expect(wrapper.vm.webhookStatusLabel).toBe('swag-paypal-settings.webhook.status.unknown'); + }); + + it('should fetch webhook status', async () => { + const wrapper = await createWrapper(); + + const spyStatus = jest.spyOn(wrapper.vm.SwagPayPalWebhookService, 'status'); + + wrapper.vm.fetchWebhookStatus(null); + + expect(wrapper.vm.status).toBe('fetching'); + expect(spyStatus).toBeCalled(); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.status).toBe('none'); + }); + + it('should refresh webhook', async () => { + const wrapper = await createWrapper(); + + const spyStatus = jest.spyOn(wrapper.vm.SwagPayPalWebhookService, 'status'); + const spyRegister = jest.spyOn(wrapper.vm.SwagPayPalWebhookService, 'register'); + + wrapper.vm.onRefreshWebhook(); + + expect(wrapper.vm.status).toBe('refreshing'); + expect(spyRegister).toBeCalled(); + + await flushPromises(); + + expect(wrapper.vm.status).toBe('none'); + + await wrapper.vm.$nextTick(); + + expect(spyStatus).toBeCalled(); + }); + + it('should refresh webhook with error', async () => { + const wrapper = await createWrapper(); + + wrapper.vm.createNotificationError = jest.fn(); + + wrapper.vm.SwagPayPalWebhookService.register = jest.fn(() => Promise.reject({ response: {} })); + + wrapper.vm.onRefreshWebhook(); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.status).toBe('refreshing'); + expect(wrapper.vm.createNotificationError).toBeCalled(); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/index.ts new file mode 100644 index 000000000..67565d769 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/index.ts @@ -0,0 +1,67 @@ +Shopware.Component.register('swag-paypal-settings-icon', () => import('./components/swag-paypal-settings-icon')); +Shopware.Component.register('swag-paypal-settings-locale-select', () => import('./components/swag-paypal-settings-locale-select')); +Shopware.Component.register('swag-paypal-settings-sales-channel-switch', () => import('./components/swag-paypal-settings-sales-channel-switch')); +Shopware.Component.register('swag-paypal-settings-webhook', () => import('./components/swag-paypal-settings-webhook')); + +Shopware.Component.register('swag-paypal-settings-advanced', () => import('./view/swag-paypal-settings-advanced')); +Shopware.Component.register('swag-paypal-settings-general', () => import('./view/swag-paypal-settings-general')); +Shopware.Component.register('swag-paypal-settings-storefront', () => import('./view/swag-paypal-settings-storefront')); + +Shopware.Component.register('swag-paypal-settings', () => import('./page/swag-paypal-settings')); + +Shopware.Module.register('swag-paypal-settings', { + type: 'plugin', + name: 'SwagPayPalSettings', + title: 'swag-paypal-settings.module.title', + description: 'swag-paypal-settings.module.description', + version: '1.0.0', + targetVersion: '1.0.0', + color: '#9AA8B5', + icon: 'regular-cog', + + routes: { + index: { + component: 'swag-paypal-settings', + path: 'index', + redirect: { name: 'swag.paypal.settings.index.general' }, + meta: { + parentPath: 'sw.settings.index.plugins', + privilege: 'swag_paypal.viewer', + }, + children: { + general: { + path: 'general', + component: 'swag-paypal-settings-general', + meta: { + privilege: 'swag_paypal.viewer', + parentPath: 'sw.settings.index', + }, + }, + storefront: { + path: 'storefront', + component: 'swag-paypal-settings-storefront', + meta: { + privilege: 'swag_paypal.viewer', + parentPath: 'sw.settings.index', + }, + }, + advanced: { + path: 'advanced', + component: 'swag-paypal-settings-advanced', + meta: { + privilege: 'swag_paypal.viewer', + parentPath: 'sw.settings.index', + }, + }, + }, + }, + }, + + settingsItem: { + group: 'plugins', + to: 'swag.paypal.settings.index', + iconComponent: 'swag-paypal-settings-icon', + backgroundEnabled: true, + privilege: 'swag_paypal.viewer', + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/index.ts new file mode 100644 index 000000000..7c1d92323 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/index.ts @@ -0,0 +1,22 @@ +import template from './swag-paypal-settings.html.twig'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-settings'), + Shopware.Mixin.getByName('swag-paypal-merchant-information'), + ], + + metaInfo() { + return { + title: this.$createTitle(), + }; + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/swag-paypal-settings.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/swag-paypal-settings.html.twig new file mode 100644 index 000000000..faf424206 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/page/swag-paypal-settings/swag-paypal-settings.html.twig @@ -0,0 +1,60 @@ + + + + + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/de-DE.json b/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/de-DE.json new file mode 100644 index 000000000..96d88eaf8 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/de-DE.json @@ -0,0 +1,114 @@ +{ + "swag-paypal": { + "notifications": { + "test": { + "title": "PayPal API-Zugangsdaten", + "errorMessage": "Die Validation ist fehlgeschlagen:
{message}", + "successMessage": "Die Zugangsdaten sind gültig." + }, + "webhook": { + "title": "PayPal Webhook", + "errorMessage": "Der Webhook konnte nicht aktualisiert werden:
{message}", + "successMessage": "Die Aktualisierung war erfolgreich." + } + } + }, + "swag-paypal-settings": { + "header": "PayPal", + "module": { + "title": "PayPal", + "description": "Einstellungen für PayPal" + }, + "tabs": { + "general": "Allgemein", + "storefront": "Storefront-Darstellung", + "advanced": "Fortgeschritten" + }, + "options": { + "intent": { + "CAPTURE": "Automatischer Zahlungseinzug (Intent CAPTURE)", + "AUTHORIZE": "Manueller Zahlungseinzug (Intent AUTHORIZE)" + }, + "landingPage": { + "LOGIN": "Anmeldung", + "GUEST_CHECKOUT": "Gast-Checkout", + "NO_PREFERENCE": "Keine Präferenz" + }, + "buttonColor": { + "blue": "Blau", + "black": "Schwarz", + "gold": "Gold (empfohlen)", + "silver": "Silber", + "white": "Weiß" + }, + "buttonShape": { + "sharp": "Rechteckig mit spitzen Ecken", + "pill": "Rund", + "rect": "Rechteckig mit abgerundeten Ecken" + } + }, + "credentialsLive": { + "title": "Live-API-Zugangsdaten", + "test": "Testen" + }, + "credentialsSandbox": { + "title": "Sandbox-API-Zugangsdaten", + "test": "Testen" + }, + "behavior": { + "title": "Verhalten" + }, + "vaulting": { + "title": "Vaulting", + "descriptionTitle": "Einmal-Paypal-Checkout", + "descriptionText": "PayPal-Kontos mit aktiviertem 'Vaulting' können kontinuierlich belastet werden, ohne dass Deine Kunden während der Transaktionen anwesend sein müssen. Auch weitere Transaktionen können durchgeführt werden, ohne dass sich Deine Kunden erneut bei PayPal authentifizieren müssen.", + "activeButtonLabel": "Vaulting freischalten" + }, + "acdc": { + "title": "Kredit- oder Debitkarte" + }, + "pui": { + "title": "Rechnungskauf" + }, + "express": { + "title": "Express Checkout Shortcut", + "subtitle": "Express Checkouts erhöhen die Konversionsrate in Deinem Shop und stellen für Dich als Händler kein finanzielles Risiko dar, daher sollten sie immer aktiv sein.", + "alertMessage": "Double Opt-In für Gast-Bestellungen ist in einem Deiner Verkaufskanäle aktiviert. Diese Funktion ist nicht mit dem PayPal Express Checkout Shortcut kompatibel. Kunden, die sich über den Express Checkout Shortcut anmelden, erhalten keine Double-Opt-In-Verifizierungs-E-Mails." + }, + "installment": { + "title": "'Später Bezahlen'-Banner", + "subtitle": "Banner erhöhen die Konversionsrate in Deinem Shop und stellen für Dich als Händler kein finanzielles Risiko dar, daher sollten sie immer aktiv sein." + }, + "spb": { + "title": "Smart Payment Buttons" + }, + "webhook": { + "title": "Webhook", + "info": "Der Webhook dient dazu, den Status einer Transaktion asynchron zu aktualisieren. Falsch registrierte Webhooks, z.B. auf einem anderen oder nicht öffentlich zugänglichen Server, können zu unbestätigten Transaktionen führen, unabhängig davon, ob die Transaktion von PayPal verarbeitet wurde.", + "buttonRefresh": "Webhook aktualisieren", + "refreshFailed": "Aktualisierung des Webhooks ist fehlgeschlagen", + "fetchFailed": "Prüfen des Webhooks ist fehlgeschlagen", + "status": { + "unknown": "Laden...", + "missing": "Fehlend", + "invalid": "Registriert, aber mit abweichender Domain", + "valid": "Gültig" + } + }, + "crossBorder": { + "title": "Später bezahlen - Länderübergreifende Nachrichten", + "warning": "Bitte wenden Sie sich an Ihren PayPal-Vertreter, um diese Funktion für Ihr Konto zu aktivieren.", + "info": "Mit länderübergreifende Nachrichten für \"Später bezahlen\" wird die \"Später bezahlen\"-Nachricht in der Sprache des Kunden anzeigt. Diese Funktion ist nur für bestimmte Länder verfügbar. {0}", + "buyerCountryAuto": "Automatische Bestimmung", + "buyerCountryOverride": "Lokalisierung" + } + }, + "swag-paypal-settings-sales-channel-switch": { + "description": "Klicken, um PayPal im ausgewählten Sales Channel zu aktivieren und als Standardzahlungsart zu setzen", + "label": "Setze PayPal als Standard" + }, + "swag-paypal-settings-locale-select": { + "invalid": "Die angegebene Sprache ist ungültig. Bitte wähle eine aus der Liste aus.", + "automatic": "Automatisch (empfohlen)" + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/en-GB.json b/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/en-GB.json new file mode 100644 index 000000000..8701f32e1 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/snippet/en-GB.json @@ -0,0 +1,114 @@ +{ + "swag-paypal": { + "notifications": { + "test": { + "title": "PayPal API credentials", + "errorMessage": "The validation failed:
{message}", + "successMessage": "The credentials are valid." + }, + "webhook": { + "title": "PayPal Webhook", + "errorMessage": "Failed to refresh webhook:
{message}", + "successMessage": "The webhook has been refreshed." + } + } + }, + "swag-paypal-settings": { + "header": "PayPal", + "module": { + "title": "PayPal", + "description": "PayPal settings" + }, + "tabs": { + "general": "General", + "storefront": "Storefront presentation", + "advanced": "Advanced" + }, + "options": { + "intent": { + "CAPTURE": "Automatic payment collection (intent CAPTURE)", + "AUTHORIZE": "Manual payment collection (intent AUTHORIZE)" + }, + "landingPage": { + "LOGIN": "Login", + "GUEST_CHECKOUT": "Guest Checkout", + "NO_PREFERENCE": "No preference" + }, + "buttonShape": { + "sharp": "Rectangular with sharp corners", + "pill": "Round", + "rect": "Rectangular with rounded corners" + }, + "buttonColor": { + "blue": "Blue", + "black": "Black", + "gold": "Gold (recommended)", + "silver": "Silver", + "white": "White" + } + }, + "credentialsLive": { + "title": "Live API credentials", + "test": "Test" + }, + "credentialsSandbox": { + "title": "Sandbox API credentials", + "test": "Test" + }, + "behavior": { + "title": "Behaviour" + }, + "vaulting": { + "title": "Vaulting", + "descriptionTitle": "One-time Paypal checkout", + "descriptionText": "Vaulting a PayPal account will allow you to charge the account in the future without requiring your customer to be present during the transaction or re-authenticate with PayPal when they are present during the transaction.", + "activeButtonLabel": "Activate Vaulting" + }, + "acdc": { + "title": "Credit- or debit card" + }, + "pui": { + "title": "Pay upon invoice" + }, + "express": { + "title": "Express Checkout Shortcut", + "subtitle": "Express Checkouts increase the conversion rate in your shop and pose no financial risk to you as a merchant. It is recommended to keep them activated. ", + "alertMessage": "Double opt-in for guests is activated in one of your Sales Channels. This feature is not compatible with PayPal Express Shortcut buttons. Customers who sign up via the Express Checkout Shortcut buttons won't receive double opt-in verification emails." + }, + "installment": { + "title": "'Pay Later' banner", + "subtitle": "Banners increase the conversion rate in your shop and pose no financial risk to you as a merchant. It is recommended to keep them activated." + }, + "spb": { + "title": "Smart Payment Buttons" + }, + "webhook": { + "title": "Webhook", + "info": "The webhook is used for updating the status of a transaction asynchronously. Incorrectly registered webhooks, e.g. on a different or not publicly available server, may result in unconfirmed transactions, regardless whether the transaction has been processed by PayPal.", + "buttonRefresh": "Refresh webhook", + "refreshFailed": "Failed to refresh webhook", + "fetchFailed": "Failed to fetch webhook status", + "status": { + "unknown": "Loading...", + "missing": "Not registered", + "invalid": "Registered, but possible domain mismatch", + "valid": "Registered" + } + }, + "crossBorder": { + "title": "Pay Later cross-border messaging", + "warning": "Please contact your PayPal representative to enable this feature for your account.", + "info": "Pay Later messaging is a feature that allows you to display a Pay Later message in the language of the customer. This feature is only available for certain countries. {0}", + "buyerCountryAuto": "Automatic determination", + "buyerCountryOverride": "Localization" + } + }, + "swag-paypal-settings-sales-channel-switch": { + "description": "Click this button to enable PayPal and select it as default in the selected Sales Channel", + "label": "Set PayPal as default" + }, + "swag-paypal-settings-locale-select": { + "invalid": "The provided locale is invalid. Please choose one from the list.", + "automatic": "Automatic (recommended)" + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/index.ts new file mode 100644 index 000000000..3036c2b07 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/index.ts @@ -0,0 +1,27 @@ +import template from './swag-paypal-settings-advanced.html.twig'; +import './swag-paypal-settings-advanced.scss'; +import { COUNTRY_OVERRIDES } from '../../../../constant/swag-paypal-settings.constant'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + countryOverrideOptions() { + const options = COUNTRY_OVERRIDES.map((locale) => ({ + value: locale, + label: this.$t(`locale.${locale}`), + })).sort((a, b) => a.label.localeCompare(b.label)); + + return [{ + value: null, + label: this.$t('swag-paypal-settings.crossBorder.buyerCountryAuto'), + }, ...options]; + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.html.twig new file mode 100644 index 000000000..30b55bbb1 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.html.twig @@ -0,0 +1,25 @@ +{% block swag_paypal_settings_webhook %} + +{% endblock %} + + + + {{ $t('swag-paypal-settings.crossBorder.warning') }} + + + + {{ $t('swag-paypal-settings.crossBorder.info') }} + + + + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.scss b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.scss new file mode 100644 index 000000000..965876609 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.scss @@ -0,0 +1,23 @@ +@import "~scss/variables"; + +.swag-paypal-settings-cross-border { + &__titles { + font-size: $font-size-m; + font-weight: $font-weight-semi-bold; + } + + &__warning-text { + width: 100%; + } + + .sw-card__content { + display: grid; + gap: 8px; + font-size: $font-size-xs; + line-height: $line-height-sm; + } + + .sw-field { + margin-bottom: 0; + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.spec.ts new file mode 100644 index 000000000..307c69c3e --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-advanced/swag-paypal-settings-advanced.spec.ts @@ -0,0 +1,75 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsAdvanced from '.'; +import type SwagPayPalSetting from 'SwagPayPal/app/component/swag-paypal-setting'; + +Shopware.Component.register('swag-paypal-settings-advanced', Promise.resolve(SwagPayPalSettingsAdvanced)); +Shopware.Component.register('swag-paypal-setting', () => import('SwagPayPal/app/component/swag-paypal-setting')); + +async function createWrapper() { + return mount( + await Shopware.Component.build('swag-paypal-settings-advanced') as typeof SwagPayPalSettingsAdvanced, + { + global: { + stubs: { + 'sw-card': await wrapTestComponent('sw-card', { sync: true }), + 'sw-card-deprecated': await wrapTestComponent('sw-card-deprecated', { sync: true }), + 'sw-alert': await wrapTestComponent('sw-alert', { sync: true }), + 'sw-alert-deprecated': await wrapTestComponent('sw-alert-deprecated', { sync: true }), + 'swag-paypal-setting': await Shopware.Component.build('swag-paypal-setting'), + 'swag-paypal-settings-webhook': true, + }, + }, + }, + ); +} + +describe('swag-paypal-settings-advanced', () => { + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should have settings cards', async () => { + const wrapper = await createWrapper(); + + const cardClasses = wrapper + .findAll('.sw-card') + .map((el) => el.classes()) + .flat() + .filter((cl) => cl.startsWith('swag-paypal')); + + expect(cardClasses).toEqual([ + 'swag-paypal-settings-cross-border', + ]); + }); + + it('should have settings', async () => { + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + expect(Object.keys(settings)).toEqual([ + 'SwagPayPal.settings.crossBorderMessagingEnabled', + 'SwagPayPal.settings.crossBorderBuyerCountry', + ]); + + expect(settings['SwagPayPal.settings.crossBorderBuyerCountry'].vm.$attrs.options) + .toBe(wrapper.vm.countryOverrideOptions); + }); + + it('should have cross-border information', async () => { + const wrapper = await createWrapper(); + + const alert = wrapper.find('.sw-alert'); + + expect(alert.exists()).toBe(true); + expect(alert.classes()).toContain('swag-paypal-settings-cross-border__warning-text'); + + const info = wrapper.find('.swag-paypal-settings-cross-border__info-text'); + + expect(info.exists()).toBe(true); + expect(info.text()).toBe('swag-paypal-settings.crossBorder.info'); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/index.ts new file mode 100644 index 000000000..2ff4df9cb --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/index.ts @@ -0,0 +1,97 @@ +import type * as PayPal from 'src/types'; +import template from './swag-paypal-settings-general.html.twig'; +import './swag-paypal-settings-general.scss'; +import { INTENTS, LANDING_PAGES } from '../../../../constant/swag-paypal-settings.constant'; + +const { Criteria } = Shopware.Data; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'acl', + 'repositoryFactory', + 'SwagPayPalSettingsService', + ], + + mixins: [ + Shopware.Mixin.getByName('swag-paypal-notification'), + ], + + data(): { + testCredentials: 'none' | 'loading' | 'success'; + testCredentialsSandbox: 'none' | 'loading' | 'success'; + } { + return { + testCredentials: 'none', + testCredentialsSandbox: 'none', + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + merchantInformationStore() { + return Shopware.Store.get('swagPayPalMerchantInformation'); + }, + + intentOptions() { + return INTENTS.map((intent) => ({ + value: intent, + label: this.$t(`swag-paypal-settings.options.intent.${intent}`), + })); + }, + + landingPageOptions() { + return LANDING_PAGES.map((landingPage) => ({ + value: landingPage, + label: this.$t(`swag-paypal-settings.options.landingPage.${landingPage}`), + })); + }, + + productRepository() { + return this.repositoryFactory.create('product'); + }, + + productStreamRepository() { + return this.repositoryFactory.create('product_stream'); + }, + + excludedProductCriteria() { + return (new Criteria(1, 25)).addAssociation('options.group'); + }, + }, + + methods: { + async onTest(prefix: 'Sandbox' | '') { + this[`testCredentials${prefix}`] = 'loading'; + + const response = await this.SwagPayPalSettingsService.testApiCredentials( + this.settingsStore.get(`SwagPayPal.settings.clientId${prefix}`), + this.settingsStore.get(`SwagPayPal.settings.clientSecret${prefix}`), + this.settingsStore.get(`SwagPayPal.settings.merchantPayerId${prefix}`), + prefix === 'Sandbox', + ).catch((error: PayPal.ServiceError) => error.response?.data ?? { errors: [] }); + + if (response.valid) { + this.createNotificationSuccess({ + title: this.$t('swag-paypal.notifications.test.title'), + message: this.$t('swag-paypal.notifications.test.successMessage'), + }); + } else if (response.errors) { + this.createNotificationError({ + title: this.$t('swag-paypal.notifications.test.title'), + message: this.$t('swag-paypal.notifications.test.errorMessage', { + message: this.createMessageFromError(response), + }), + }); + } + + this[`testCredentials${prefix}`] = response.valid ? 'success' : 'none'; + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.html.twig new file mode 100644 index 000000000..dd4a5b005 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.html.twig @@ -0,0 +1,229 @@ +{% block swag_paypal_settings_live_credentials %} + + + + + + + + + + + +{% endblock %} + +{% block swag_paypal_settings_sandbox_credentials %} + + + + + + + + + + + +{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ $t('swag-paypal-settings.vaulting.descriptionTitle') }} +
+ +
+ {{ $t('swag-paypal-settings.vaulting.descriptionText') }} +
+ + +
+ + + + + + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.scss b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.scss new file mode 100644 index 000000000..98228aa0e --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.scss @@ -0,0 +1,20 @@ +@import "~scss/variables"; + +.swag-paypal-settings { + &-vaulting { + &__description-title { + color: $color-darkgray-200; + font-weight: $font-weight-bold; + font-size: $font-size-m; + margin-bottom: 16px; + } + + &__description-text { + margin-bottom: 24px; + } + + .swag-paypal-settings.is--boolean { + margin-bottom: 24px; + } + } +} diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.spec.ts new file mode 100644 index 000000000..75fd1b180 --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-general/swag-paypal-settings-general.spec.ts @@ -0,0 +1,154 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsGeneral from '.'; +import SettingsFixture from '../../../../app/store/settings.fixture'; +import type SwagPayPalSetting from 'SwagPayPal/app/component/swag-paypal-setting'; + +Shopware.Component.register('swag-paypal-settings-general', Promise.resolve(SwagPayPalSettingsGeneral)); +Shopware.Component.register('swag-paypal-setting', () => import('SwagPayPal/app/component/swag-paypal-setting')); + +async function createWrapper() { + return mount( + await Shopware.Component.build('swag-paypal-settings-general') as typeof SwagPayPalSettingsGeneral, + { + global: { + provide: { acl: { can: () => true } }, + mocks: { $t: (key: string) => key }, + stubs: { + 'swag-paypal-setting': await Shopware.Component.build('swag-paypal-setting'), + 'sw-inherit-wrapper': await wrapTestComponent('sw-inherit-wrapper', { sync: true }), + 'sw-switch-field': await wrapTestComponent('sw-switch-field', { sync: true }), + 'sw-switch-field-deprecated': await wrapTestComponent('sw-switch-field-deprecated', { sync: true }), + 'sw-checkbox-field': await wrapTestComponent('sw-checkbox-field', { sync: true }), + 'sw-checkbox-field-deprecated': await wrapTestComponent('sw-checkbox-field-deprecated', { sync: true }), + + 'sw-card': await wrapTestComponent('sw-card', { sync: true }), + 'sw-card-deprecated': await wrapTestComponent('sw-card-deprecated', { sync: true }), + 'sw-base-field': await wrapTestComponent('sw-base-field', { sync: true }), + }, + }, + }, + ); +} + +describe('swag-paypal-settings-general', () => { + const store = Shopware.Store.get('swagPayPalSettings'); + + beforeEach(() => { + store.setConfig(null, SettingsFixture.Default); + }); + + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should have settings cards', async () => { + const wrapper = await createWrapper(); + + const cardClasses = wrapper + .findAll('.sw-card') + .map((el) => el.classes()) + .flat() + .filter((cl) => cl.startsWith('swag-paypal')); + + expect(cardClasses).toEqual([ + 'swag-paypal-settings-live-credentials', + 'swag-paypal-settings-sandbox-credentials', + 'swag-paypal-settings-behavior', + 'swag-paypal-settings-vaulting', + 'swag-paypal-settings-acdc', + 'swag-paypal-settings-pui', + ]); + }); + + it('should have settings', async () => { + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + expect(Object.keys(settings)).toEqual([ + 'SwagPayPal.settings.sandbox', + 'SwagPayPal.settings.clientId', + 'SwagPayPal.settings.clientSecret', + 'SwagPayPal.settings.merchantPayerId', + 'SwagPayPal.settings.clientIdSandbox', + 'SwagPayPal.settings.clientSecretSandbox', + 'SwagPayPal.settings.merchantPayerIdSandbox', + 'SwagPayPal.settings.intent', + 'SwagPayPal.settings.submitCart', + 'SwagPayPal.settings.brandName', + 'SwagPayPal.settings.landingPage', + 'SwagPayPal.settings.sendOrderNumber', + 'SwagPayPal.settings.orderNumberPrefix', + 'SwagPayPal.settings.orderNumberSuffix', + 'SwagPayPal.settings.excludedProductIds', + 'SwagPayPal.settings.excludedProductStreamIds', + 'SwagPayPal.settings.acdcForce3DS', + 'SwagPayPal.settings.puiCustomerServiceInstructions', + ]); + }); + + it('should invert sandbox toggle for live and sandbox', async () => { + const wrapper = await createWrapper(); + + const liveSwitch = wrapper.findComponent('.swag-paypal-settings-live-credentials .sw-field--switch'); + expect(liveSwitch.exists()).toBe(true); + + const sandboxSwitch = wrapper.findComponent('.swag-paypal-settings-sandbox-credentials .sw-field--switch'); + expect(sandboxSwitch.exists()).toBe(true); + + expect(store.isSandbox).toBe(false); + expect(liveSwitch.vm.value).toBe(true); + expect(sandboxSwitch.vm.value).toBe(false); + + // Switch trough store + store.set('SwagPayPal.settings.sandbox', true); + + await wrapper.vm.$nextTick(); + + expect(liveSwitch.vm.value).toBe(false); + expect(sandboxSwitch.vm.value).toBe(true); + + // Switch trough UI + await sandboxSwitch.find('input').setValue(false); + expect(liveSwitch.vm.value).toBe(true); + expect(sandboxSwitch.vm.value).toBe(false); + + await liveSwitch.find('input').setValue(false); + expect(liveSwitch.vm.value).toBe(false); + expect(sandboxSwitch.vm.value).toBe(true); + }); + + it('should disable credentials fields on sandbox toggle', async () => { + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + const live = [ + 'SwagPayPal.settings.clientId', + 'SwagPayPal.settings.clientSecret', + 'SwagPayPal.settings.merchantPayerId', + ]; + + const sandbox = [ + 'SwagPayPal.settings.clientIdSandbox', + 'SwagPayPal.settings.clientSecretSandbox', + 'SwagPayPal.settings.merchantPayerIdSandbox', + ]; + + store.set('SwagPayPal.settings.sandbox', true); + await wrapper.vm.$nextTick(); + + expect(live.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(true); + expect(sandbox.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(false); + + store.set('SwagPayPal.settings.sandbox', false); + await wrapper.vm.$nextTick(); + + expect(live.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(false); + expect(sandbox.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(true); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/index.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/index.ts new file mode 100644 index 000000000..d01dec5aa --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/index.ts @@ -0,0 +1,68 @@ +import template from './swag-paypal-settings-storefront.html.twig'; +import { BUTTON_COLORS, BUTTON_SHAPES } from '../../../../constant/swag-paypal-settings.constant'; + +export default Shopware.Component.wrapComponentConfig({ + template, + + compatConfig: Shopware.compatConfig, + + inject: [ + 'systemConfigApiService', + ], + + data() { + return { + doubleOptInConfig: false, + }; + }, + + computed: { + settingsStore() { + return Shopware.Store.get('swagPayPalSettings'); + }, + + buttonColorOptions() { + return BUTTON_COLORS.map((color) => ({ + value: color, + label: this.$t(`swag-paypal-settings.options.buttonColor.${color}`), + })); + }, + + buttonShapeOptions() { + return BUTTON_SHAPES.map((shape) => ({ + value: shape, + label: this.$t(`swag-paypal-settings.options.buttonShape.${shape}`), + })); + }, + + sbpSettingsDisabled(): boolean { + return !this.settingsStore.salesChannel && !this.settingsStore.getActual('SwagPayPal.settings.spbCheckoutEnabled'); + }, + + ecsSettingsDisabled(): boolean { + return !this.settingsStore.salesChannel + && !this.settingsStore.getActual('SwagPayPal.settings.ecsDetailEnabled') + && !this.settingsStore.getActual('SwagPayPal.settings.ecsCartEnabled') + && !this.settingsStore.getActual('SwagPayPal.settings.ecsOffCanvasEnabled') + && !this.settingsStore.getActual('SwagPayPal.settings.ecsLoginEnabled') + && !this.settingsStore.getActual('SwagPayPal.settings.ecsListingEnabled'); + }, + }, + + watch: { + 'settingsStore.salesChannel': { + immediate: true, + handler() { + this.fetchDoubleOptIn(); + }, + }, + }, + + methods: { + async fetchDoubleOptIn() { + const config = await this.systemConfigApiService.getValues('core.loginRegistration.doubleOptInGuestOrder') as Record; + + this.doubleOptInConfig = !!config['core.loginRegistration.doubleOptInGuestOrder']; + }, + }, +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.html.twig b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.html.twig new file mode 100644 index 000000000..3059a520d --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.html.twig @@ -0,0 +1,113 @@ + + + {{ $t('swag-paypal-settings.express.alertMessage') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.spec.ts b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.spec.ts new file mode 100644 index 000000000..f9f09b67b --- /dev/null +++ b/src/Resources/app/administration/src/module/swag-paypal-settings/view/swag-paypal-settings-storefront/swag-paypal-settings-storefront.spec.ts @@ -0,0 +1,140 @@ +import { mount } from '@vue/test-utils'; +import SwagPayPalSettingsStorefront from '.'; +import SwagPayPalSetting from 'SwagPayPal/app/component/swag-paypal-setting'; +import { SYSTEM_CONFIGS } from '../../../../constant/swag-paypal-settings.constant'; +import SettingsFixture from '../../../../app/store/settings.fixture'; + +Shopware.Component.register('swag-paypal-settings-storefront', Promise.resolve(SwagPayPalSettingsStorefront)); +Shopware.Component.register('swag-paypal-setting', Promise.resolve(SwagPayPalSetting)); + +async function createWrapper() { + return mount( + await Shopware.Component.build('swag-paypal-settings-storefront') as typeof SwagPayPalSettingsStorefront, + { + global: { + stubs: { + 'sw-card': await wrapTestComponent('sw-card', { sync: true }), + 'sw-card-deprecated': await wrapTestComponent('sw-card-deprecated', { sync: true }), + 'swag-paypal-setting': await Shopware.Component.build('swag-paypal-setting'), + }, + provide: { + acl: { can: () => true }, + systemConfigApiService: { getValues: () => false }, + }, + }, + }, + ); +} + +describe('swag-paypal-settings-storefront', () => { + const store = Shopware.Store.get('swagPayPalSettings'); + + it('should be a Vue.js component', async () => { + const wrapper = await createWrapper(); + + expect(wrapper.vm).toBeTruthy(); + }); + + it('should have settings cards', async () => { + const wrapper = await createWrapper(); + + const cardClasses = wrapper + .findAll('.sw-card') + .map((el) => el.classes()) + .flat() + .filter((cl) => cl.startsWith('swag-paypal')); + + expect(cardClasses).toEqual([ + 'swag-paypal-settings-express', + 'swag-paypal-settings-installment', + 'swag-paypal-settings-spb', + ]); + }); + + it('should have settings', async () => { + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + expect(Object.keys(settings)).toEqual([ + 'SwagPayPal.settings.ecsDetailEnabled', + 'SwagPayPal.settings.ecsCartEnabled', + 'SwagPayPal.settings.ecsOffCanvasEnabled', + 'SwagPayPal.settings.ecsLoginEnabled', + 'SwagPayPal.settings.ecsListingEnabled', + 'SwagPayPal.settings.ecsButtonColor', + 'SwagPayPal.settings.ecsButtonShape', + 'SwagPayPal.settings.ecsButtonLanguageIso', + 'SwagPayPal.settings.ecsShowPayLater', + 'SwagPayPal.settings.installmentBannerDetailPageEnabled', + 'SwagPayPal.settings.installmentBannerCartEnabled', + 'SwagPayPal.settings.installmentBannerOffCanvasCartEnabled', + 'SwagPayPal.settings.installmentBannerLoginPageEnabled', + 'SwagPayPal.settings.installmentBannerFooterEnabled', + 'SwagPayPal.settings.spbCheckoutEnabled', + 'SwagPayPal.settings.spbAlternativePaymentMethodsEnabled', + 'SwagPayPal.settings.spbShowPayLater', + 'SwagPayPal.settings.spbButtonColor', + 'SwagPayPal.settings.spbButtonShape', + 'SwagPayPal.settings.spbButtonLanguageIso', + ]); + }); + + it('should disable ecs fields based on ecsSettingsDisabled', async () => { + store.setConfig(null, SettingsFixture.Default); + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + const disabledSettings = [ + 'SwagPayPal.settings.ecsButtonColor', + 'SwagPayPal.settings.ecsButtonShape', + 'SwagPayPal.settings.ecsButtonLanguageIso', + 'SwagPayPal.settings.ecsShowPayLater', + ]; + + // enable all + SYSTEM_CONFIGS.filter((setting) => setting.startsWith('SwagPayPal.settings.ecs')).forEach((setting) => store.set(setting, true)); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.ecsSettingsDisabled).toBe(false); + expect(disabledSettings.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(false); + + // disable all + SYSTEM_CONFIGS.filter((setting) => setting.startsWith('SwagPayPal.settings.ecs')).forEach((setting) => store.set(setting, false)); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.ecsSettingsDisabled).toBe(true); + expect(disabledSettings.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(true); + }); + + it('should disable spb fields based on spbCheckoutEnabled', async () => { + store.setConfig(null, SettingsFixture.Default); + const wrapper = await createWrapper(); + + const components = wrapper.findAllComponents({ name: 'swag-paypal-setting' }); + const settings = Object.fromEntries(components.map((el) => [el.props().path, el])); + + const disabledSettings = [ + 'SwagPayPal.settings.spbAlternativePaymentMethodsEnabled', + 'SwagPayPal.settings.spbShowPayLater', + 'SwagPayPal.settings.spbButtonColor', + 'SwagPayPal.settings.spbButtonShape', + 'SwagPayPal.settings.spbButtonLanguageIso', + ]; + + store.set('SwagPayPal.settings.spbCheckoutEnabled', true); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.sbpSettingsDisabled).toBe(false); + expect(disabledSettings.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(false); + + store.set('SwagPayPal.settings.spbCheckoutEnabled', false); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.sbpSettingsDisabled).toBe(true); + expect(disabledSettings.map((setting) => settings[setting]?.vm.formAttrs.disabled)).toContain(true); + }); +}); diff --git a/src/Resources/app/administration/src/module/swag-paypal/acl/index.ts b/src/Resources/app/administration/src/module/swag-paypal/acl/index.ts index 9a7ba0fc0..61024854a 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/acl/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/acl/index.ts @@ -1,3 +1,6 @@ +/** + * @deprecated tag:v10.0.0 - Will be moved to app directory + */ Shopware.Service('privileges').addPrivilegeMappingEntry({ category: 'permissions', parent: 'swag_paypal', @@ -27,6 +30,9 @@ Shopware.Service('privileges').addPrivilegeMappingEntry({ }, }); +/** + * @deprecated tag:v10.0.0 - Will be moved to app directory + */ Shopware.Service('privileges').addPrivilegeMappingEntry({ category: 'permissions', parent: null, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-acdc/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-acdc/index.ts index 605915a75..e0e9ab81f 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-acdc/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-acdc/index.ts @@ -1,6 +1,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-acdc.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-general` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-behavior/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-behavior/index.ts index 1143603cc..755eff648 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-behavior/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-behavior/index.ts @@ -4,6 +4,9 @@ import constants from '../../page/swag-paypal/swag-paypal-consts'; const { Criteria } = Shopware.Data; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-general` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-domain-association/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-domain-association/index.ts index 0a141f0ff..f7740baae 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-domain-association/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-domain-association/index.ts @@ -1,6 +1,9 @@ import template from './swag-paypal-checkout-domain-association.html.twig'; import './swag-paypal-checkout-domain-association.scss'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-method-domain-association` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-method/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-method/index.ts index 0b33e1160..27b429ccf 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-method/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout-method/index.ts @@ -3,6 +3,9 @@ import './swag-paypal-checkout-method.scss'; const { Context } = Shopware; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-payment-method` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout/index.ts index d96be983d..435bb9118 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-checkout/index.ts @@ -5,6 +5,9 @@ import './swag-paypal-checkout.scss'; const { Context } = Shopware; const { Criteria } = Shopware.Data; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-method-card` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-created-component-helper/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-created-component-helper/index.ts index 119e901b9..d75c5db8b 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-created-component-helper/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-created-component-helper/index.ts @@ -7,6 +7,9 @@ import template from './swag-paypal-created-component-helper.html.twig'; * race conditions can occur here. */ +/** + * @deprecated tag:v10.0.0 - Will be removed without replacement + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-credentials/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-credentials/index.ts index 2a866da7d..6a6f06c21 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-credentials/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-credentials/index.ts @@ -1,6 +1,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-credentials.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-general` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-cross-border/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-cross-border/index.ts index 3ac4cd84a..848c77eca 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-cross-border/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-cross-border/index.ts @@ -2,6 +2,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-cross-border.html.twig'; import './swag-paypal-cross-border.scss'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-advanced` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-express/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-express/index.ts index 8b45f13cc..6f9a41377 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-express/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-express/index.ts @@ -2,6 +2,10 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-express.html.twig'; const { Criteria } = Shopware.Data; + +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-storefront` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-inherit-wrapper/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-inherit-wrapper/index.ts index d3065d2ac..c68e0d100 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-inherit-wrapper/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-inherit-wrapper/index.ts @@ -4,6 +4,8 @@ import template from './swag-paypal-inherit-wrapper.html.twig'; /** * @private + * + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-setting` */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-installment/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-installment/index.ts index 88ecd74c1..b48c09ecf 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-installment/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-installment/index.ts @@ -1,6 +1,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-installment.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-storefront` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-locale-field/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-locale-field/index.ts index e6fce1b07..12ba4f8a3 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-locale-field/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-locale-field/index.ts @@ -6,6 +6,9 @@ const { debounce } = Shopware.Utils; type ValueEvent = { target: { value?: string } }; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-locale-select` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-plugin-box-with-onboarding/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-plugin-box-with-onboarding/index.ts index 19b074218..660f0e15f 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-plugin-box-with-onboarding/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-plugin-box-with-onboarding/index.ts @@ -1,5 +1,8 @@ import template from './sw-plugin-box-with-onboarding.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be removed without replacement + */ Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-pui/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-pui/index.ts index 4f88ab016..a5881d034 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-pui/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-pui/index.ts @@ -1,6 +1,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-pui.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-general` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-hint/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-hint/index.ts index 0ab4aae1f..a10f8e770 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-hint/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-hint/index.ts @@ -1,6 +1,9 @@ import './swag-paypal-settings-hint.scss'; import template from './swag-paypal-settings-hint.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be removed without replacement + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-icon/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-icon/index.ts index 4cb44d09d..60adad637 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-icon/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-settings-icon/index.ts @@ -1,6 +1,9 @@ import template from './swag-paypal-settings-icon.html.twig'; import './swag-paypal-settings-icon.scss'; +/** + * @deprecated tag:v10.0.0 - Will be moved to `swag-paypal-settings` module + */ export default Shopware.Component.wrapComponentConfig({ template, }); diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-spb/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-spb/index.ts index cf4020f08..ecc2a5100 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-spb/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-spb/index.ts @@ -1,6 +1,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-spb.html.twig'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-storefront` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-vaulting/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-vaulting/index.ts index 9aa127319..606e5ec99 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-vaulting/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-vaulting/index.ts @@ -2,6 +2,9 @@ import type * as PayPal from 'src/types'; import template from './swag-paypal-vaulting.html.twig'; import './swag-paypal-vaulting.scss'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-general` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-webhook/index.ts b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-webhook/index.ts index af8a02652..a8767541c 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-webhook/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/components/swag-paypal-webhook/index.ts @@ -6,6 +6,9 @@ const STATUS_WEBHOOK_MISSING = 'missing'; const STATUS_WEBHOOK_INVALID = 'invalid'; const STATUS_WEBHOOK_VALID = 'valid'; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings-webhook` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/module/swag-paypal/index.ts b/src/Resources/app/administration/src/module/swag-paypal/index.ts index 816356a00..92b61eb6f 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/index.ts @@ -24,6 +24,9 @@ Shopware.Component.register('swag-paypal', () => import('./page/swag-paypal')); const { Module } = Shopware; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings` + */ Module.register('swag-paypal', { type: 'plugin', name: 'SwagPayPal', diff --git a/src/Resources/app/administration/src/module/swag-paypal/page/swag-paypal/index.ts b/src/Resources/app/administration/src/module/swag-paypal/page/swag-paypal/index.ts index 5a43b96e7..80eb9b10a 100644 --- a/src/Resources/app/administration/src/module/swag-paypal/page/swag-paypal/index.ts +++ b/src/Resources/app/administration/src/module/swag-paypal/page/swag-paypal/index.ts @@ -12,6 +12,9 @@ type ConfigComponent = { const { Defaults } = Shopware; const { Criteria } = Shopware.Data; +/** + * @deprecated tag:v10.0.0 - Will be replaced by `swag-paypal-settings` + */ export default Shopware.Component.wrapComponentConfig({ template, diff --git a/src/Resources/app/administration/src/types/openapi.d.ts b/src/Resources/app/administration/src/types/openapi.d.ts index dfad4121a..e218e8daa 100644 --- a/src/Resources/app/administration/src/types/openapi.d.ts +++ b/src/Resources/app/administration/src/types/openapi.d.ts @@ -108,12 +108,18 @@ export interface paths { "/api/_action/paypal/validate-api-credentials": { get: operations["validateApiCredentials"]; }; + "/api/_action/paypal/test-api-credentials": { + post: operations["testApiCredentials"]; + }; "/api/_action/paypal/get-api-credentials": { post: operations["getApiCredentials"]; }; "/api/_action/paypal/merchant-information": { get: operations["getMerchantInformation"]; }; + "/api/_action/paypal/save-settings": { + post: operations["saveSettings"]; + }; "/api/_action/paypal/webhook/status/{salesChannelId}": { get: operations["getWebhookStatus"]; }; @@ -1391,12 +1397,19 @@ export interface components { remoteCount: number; }; swag_paypal_setting_merchant_information: { - merchantIntegrations: components["schemas"]["swag_paypal_v1_merchant_integrations"]; + merchantIntegrations: components["schemas"]["swag_paypal_v1_merchant_integrations"] | null; /** @description string> key: paymentMethodId, value: capability (see AbstractMethodData) */ capabilities: { [key: string]: string; }; }; + swag_paypal_setting_settings_information: { + sandboxCredentialsChanged: boolean; + sandboxCredentialsValid: boolean | null; + liveCredentialsChanged: boolean; + liveCredentialsValid: boolean | null; + webhookErrors: string[]; + }; }; responses: never; parameters: never; @@ -1993,6 +2006,19 @@ export interface operations { }; }; }; + testApiCredentials: { + responses: { + /** @description Returns if the provided API credentials are valid */ + 200: { + content: { + "application/json": { + valid: boolean; + errors: components["schemas"]["error"][]; + }; + }; + }; + }; + }; getApiCredentials: { requestBody?: { content: { @@ -2031,6 +2057,18 @@ export interface operations { }; }; }; + saveSettings: { + responses: { + /** @description Returns information about the saved settings */ + 200: { + content: { + "application/json": { + [key: string]: components["schemas"]["swag_paypal_setting_settings_information"]; + }; + }; + }; + }; + }; getWebhookStatus: { parameters: { path: { diff --git a/src/Resources/app/administration/src/types/system-config.ts b/src/Resources/app/administration/src/types/system-config.ts index dd8ff973d..07dc42467 100644 --- a/src/Resources/app/administration/src/types/system-config.ts +++ b/src/Resources/app/administration/src/types/system-config.ts @@ -1,9 +1,17 @@ -export const LANDING_PAGES = ['LOGIN', 'BILLING', 'NO_PREFERENCE'] as const; -export const BUTTON_COLORS = ['gold', 'blue', 'black', 'silver', 'white'] as const; -export const BUTTON_SHAPES = ['rect', 'pill', 'sharp'] as const; -export const INTENTS = ['CAPTURE', 'AUTHORIZE'] as const; -export const COUNTRY_OVERRIDES = ['en-AU', 'de-DE', 'es-ES', 'fr-FR', 'en-GB', 'it-IT', 'en-US'] as const; +import type { SYSTEM_CONFIG, LANDING_PAGES, BUTTON_COLORS, BUTTON_SHAPES, INTENTS, COUNTRY_OVERRIDES } from 'src/constant/swag-paypal-settings.constant'; +/** + * @deprecated tag:v10.0.0 - Will be moved to constant/swag-paypal-settings.constant.ts. + */ +export { + LANDING_PAGES, + BUTTON_COLORS, + BUTTON_SHAPES, + INTENTS, + COUNTRY_OVERRIDES, +}; + +// @todo - Keys should be from SYSTEM_CONFIG export declare type SystemConfig = { 'SwagPayPal.settings.clientId'?: string; 'SwagPayPal.settings.clientSecret'?: string; @@ -70,7 +78,7 @@ export declare type SystemConfig = { /** * @private */ -export const SystemConfigDefinition: Record = { +export const SystemConfigDefinition: Record = { 'SwagPayPal.settings.clientId': 'string', 'SwagPayPal.settings.clientSecret': 'string', 'SwagPayPal.settings.clientIdSandbox': 'string', diff --git a/src/Resources/app/administration/src/types/window-paypal.ts b/src/Resources/app/administration/src/types/window-paypal.ts index ec916c3c2..a713b8a45 100644 --- a/src/Resources/app/administration/src/types/window-paypal.ts +++ b/src/Resources/app/administration/src/types/window-paypal.ts @@ -10,6 +10,7 @@ export declare type AppSignup = { }; render: () => void; + setup: () => void; timeout?: NodeJS.Timeout; }; @@ -28,8 +29,17 @@ declare global { interface Window { PAYPAL?: PAYPAL; + /** + * @deprecated tag:v10.0.0 - Will be removed. + */ onboardingCallbackLive?: (authCode: string, sharedId: string) => void; + + /** + * @deprecated tag:v10.0.0 - Will be removed. + */ onboardingCallbackSandbox?: (authCode: string, sharedId: string) => void; + + [key: `onboardingCallback${string}`]: undefined | ((authCode: string, sharedId: string) => void); } } diff --git a/src/Resources/config/packages/feature.yaml b/src/Resources/config/packages/feature.yaml new file mode 100644 index 000000000..1b996060a --- /dev/null +++ b/src/Resources/config/packages/feature.yaml @@ -0,0 +1,7 @@ +shopware: + feature: + flags: + - name: PAYPAL_SETTINGS_TWEAKS + default: false + major: false + description: 'PayPal Admin rewrite. Contains a lot of small improvements, but could cause problem with plugins extending PayPal.' diff --git a/src/Resources/config/services/setting.xml b/src/Resources/config/services/setting.xml index c295c56b1..75b008652 100644 --- a/src/Resources/config/services/setting.xml +++ b/src/Resources/config/services/setting.xml @@ -8,6 +8,8 @@ + + @@ -37,5 +39,11 @@ + + + + + + diff --git a/src/Resources/config/services/webhook.xml b/src/Resources/config/services/webhook.xml index 583b87d25..555e4c864 100644 --- a/src/Resources/config/services/webhook.xml +++ b/src/Resources/config/services/webhook.xml @@ -123,6 +123,8 @@ + + diff --git a/src/Setting/Service/ApiCredentialService.php b/src/Setting/Service/ApiCredentialService.php index 1a7d53cab..9d954fa90 100644 --- a/src/Setting/Service/ApiCredentialService.php +++ b/src/Setting/Service/ApiCredentialService.php @@ -43,6 +43,7 @@ public function __construct( * @deprecated tag:v10.0.0 - parameter $merchantPayerId will be added * * @throws PayPalInvalidApiCredentialsException + * @throws PayPalApiException */ public function testApiCredentials(string $clientId, string $clientSecret, bool $sandboxActive /* , ?string $merchantPayerId */): bool { diff --git a/src/Setting/Service/SettingsSaver.php b/src/Setting/Service/SettingsSaver.php new file mode 100644 index 000000000..9d4713029 --- /dev/null +++ b/src/Setting/Service/SettingsSaver.php @@ -0,0 +1,165 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Swag\PayPal\Setting\Service; + +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\System\SystemConfig\SystemConfigService; +use Swag\PayPal\Setting\Settings; +use Swag\PayPal\Setting\Struct\SettingsInformationStruct; +use Swag\PayPal\Webhook\Registration\WebhookSystemConfigHelper; + +#[Package('checkout')] +class SettingsSaver implements SettingsSaverInterface +{ + /** + * @internal + */ + public function __construct( + private readonly SystemConfigService $systemConfigService, + private readonly ApiCredentialService $apiCredentialService, + private readonly WebhookSystemConfigHelper $webhookSystemConfigHelper, + ) { + } + + /** + * @param array $settings + */ + public function save(array $settings, ?string $salesChannelId = null): SettingsInformationStruct + { + $information = new SettingsInformationStruct(); + + $old = $this->getCredentials($salesChannelId); + + $information->setLiveCredentialsChanged( + $this->credentialsChanged($settings, $old, Settings::LIVE_CREDENTIAL_KEYS), + ); + $information->setSandboxCredentialsChanged( + $this->credentialsChanged($settings, $old, Settings::SANDBOX_CREDENTIAL_KEYS), + ); + + if ($information->getLiveCredentialsChanged()) { + $information->setLiveCredentialsValid( + $this->testCredentialsLive($settings, $salesChannelId), + ); + } + + if ($information->getSandboxCredentialsChanged()) { + $information->setSandboxCredentialsValid( + $this->testCredentialsSandbox($settings, $salesChannelId), + ); + } + + if ($information->getLiveCredentialsValid() || $information->getSandboxCredentialsValid()) { + $webhookErrors = $this->webhookSystemConfigHelper->checkWebhookBefore([$salesChannelId => $settings]); + } + + $this->systemConfigService->setMultiple($settings, $salesChannelId); + + if ($information->getLiveCredentialsValid() || $information->getSandboxCredentialsValid()) { + $webhookErrors = \array_merge( + $webhookErrors ?? [], + $this->webhookSystemConfigHelper->checkWebhookAfter([$salesChannelId]), + ); + } + + $information->setWebhookErrors(\array_map(static fn (\Throwable $e) => $e->getMessage(), $webhookErrors ?? [])); + + return $information; + } + + /** + * @param array $settings + */ + private function testCredentialsLive(array $settings, ?string $salesChannelId): bool + { + $credentials = $this->filterSettings($settings, Settings::LIVE_CREDENTIAL_KEYS); + + // Inherit potentially missing credentials + if ($salesChannelId) { + $credentials[Settings::CLIENT_ID] ??= $this->systemConfigService->get(Settings::CLIENT_ID); + $credentials[Settings::CLIENT_SECRET] ??= $this->systemConfigService->get(Settings::CLIENT_SECRET); + $credentials[Settings::MERCHANT_PAYER_ID] ??= $this->systemConfigService->get(Settings::MERCHANT_PAYER_ID); + } + + return $this->testCredentials( + $credentials[Settings::CLIENT_ID] ?? '', + $credentials[Settings::CLIENT_SECRET] ?? '', + $credentials[Settings::MERCHANT_PAYER_ID] ?? '', + false, + ); + } + + /** + * @param array $settings + */ + private function testCredentialsSandbox(array $settings, ?string $salesChannelId): bool + { + $credentials = $this->filterSettings($settings, Settings::SANDBOX_CREDENTIAL_KEYS); + + // Inherit potentially missing credentials + if ($salesChannelId) { + $credentials[Settings::CLIENT_ID_SANDBOX] ??= $this->systemConfigService->get(Settings::CLIENT_ID_SANDBOX); + $credentials[Settings::CLIENT_SECRET_SANDBOX] ??= $this->systemConfigService->get(Settings::CLIENT_SECRET_SANDBOX); + $credentials[Settings::MERCHANT_PAYER_ID_SANDBOX] ??= $this->systemConfigService->get(Settings::MERCHANT_PAYER_ID_SANDBOX); + } + + return $this->testCredentials( + $credentials[Settings::CLIENT_ID_SANDBOX] ?? '', + $credentials[Settings::CLIENT_SECRET_SANDBOX] ?? '', + $credentials[Settings::MERCHANT_PAYER_ID_SANDBOX] ?? '', + true, + ); + } + + private function testCredentials(string $clientId, string $clientSecret, string $merchantId, bool $sandbox): bool + { + try { + return $this->apiCredentialService->testApiCredentials($clientId, $clientSecret, $sandbox, $merchantId); + } catch (\Exception $e) { + return false; + } + } + + private function getCredentials(?string $salesChannelId): array + { + return \array_combine( + Settings::CREDENTIAL_KEYS, + \array_map( + fn (string $key) => $this->systemConfigService->get($key, $salesChannelId), + Settings::CREDENTIAL_KEYS, + ), + ); + } + + /** + * @param array $new + * @param array $old + * @param list $keys + */ + private function credentialsChanged(array $new, array $old, array $keys): bool + { + $new = $this->filterSettings($new, $keys); + $old = $this->filterSettings($old, $keys); + + return \count(\array_diff($new, $old)) > 0; + } + + /** + * @param array $kvs + * @param list $keys + * + * @return array + */ + private function filterSettings(array $kvs, array $keys): array + { + return \array_combine($keys, \array_map( + static fn (string $key) => $kvs[$key] ?? null, + $keys, + )); + } +} diff --git a/src/Setting/Service/SettingsSaverInterface.php b/src/Setting/Service/SettingsSaverInterface.php new file mode 100644 index 000000000..0872f7681 --- /dev/null +++ b/src/Setting/Service/SettingsSaverInterface.php @@ -0,0 +1,17 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Swag\PayPal\Setting\Service; + +use Shopware\Core\Framework\Log\Package; +use Swag\PayPal\Setting\Struct\SettingsInformationStruct; + +#[Package('checkout')] +interface SettingsSaverInterface +{ + public function save(array $settings, ?string $salesChannelId = null): SettingsInformationStruct; +} diff --git a/src/Setting/Settings.php b/src/Setting/Settings.php index 398178244..450c2cdf3 100644 --- a/src/Setting/Settings.php +++ b/src/Setting/Settings.php @@ -134,6 +134,23 @@ final class Settings self::MERCHANT_LOCATION_OTHER, ]; + public const LIVE_CREDENTIAL_KEYS = [ + Settings::CLIENT_ID, + Settings::CLIENT_SECRET, + Settings::MERCHANT_PAYER_ID, + ]; + + public const SANDBOX_CREDENTIAL_KEYS = [ + Settings::CLIENT_ID_SANDBOX, + Settings::CLIENT_SECRET_SANDBOX, + Settings::MERCHANT_PAYER_ID_SANDBOX, + ]; + + public const CREDENTIAL_KEYS = [ + ...self::LIVE_CREDENTIAL_KEYS, + ...self::SANDBOX_CREDENTIAL_KEYS, + ]; + private function __construct() { } diff --git a/src/Setting/SettingsController.php b/src/Setting/SettingsController.php index 9168c7a23..4bec4bd04 100644 --- a/src/Setting/SettingsController.php +++ b/src/Setting/SettingsController.php @@ -8,13 +8,18 @@ namespace Swag\PayPal\Setting; use OpenApi\Attributes as OA; +use Shopware\Core\Framework\Api\EventListener\ErrorResponseFactory; use Shopware\Core\Framework\Context; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Routing\RoutingException; use Shopware\Core\Framework\Validation\DataBag\RequestDataBag; +use Shopware\Core\System\SystemConfig\Validation\SystemConfigValidator; +use Swag\PayPal\RestApi\Exception\PayPalApiException; use Swag\PayPal\Setting\Service\ApiCredentialServiceInterface; use Swag\PayPal\Setting\Service\MerchantIntegrationsService; +use Swag\PayPal\Setting\Service\SettingsSaverInterface; use Swag\PayPal\Setting\Struct\MerchantInformationStruct; +use Swag\PayPal\Setting\Struct\SettingsInformationStruct; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -31,6 +36,8 @@ class SettingsController extends AbstractController public function __construct( private readonly ApiCredentialServiceInterface $apiCredentialService, private readonly MerchantIntegrationsService $merchantIntegrationsService, + private readonly SystemConfigValidator $systemConfigValidator, + private readonly SettingsSaverInterface $settingsSaver, ) { } @@ -96,6 +103,63 @@ public function validateApiCredentials(Request $request): JsonResponse return new JsonResponse(['credentialsValid' => $credentialsValid]); } + #[OA\Post( + path: '/api/_action/paypal/test-api-credentials', + operationId: 'testApiCredentials', + tags: ['Admin Api', 'PayPal'], + responses: [new OA\Response( + response: Response::HTTP_OK, + description: 'Returns if the provided API credentials are valid', + content: new OA\JsonContent( + required: ['valid', 'errors'], + properties: [ + new OA\Property( + property: 'valid', + type: 'boolean', + ), + new OA\Property( + property: 'errors', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/error'), + ), + ] + ) + )] + )] + #[Route(path: '/api/_action/paypal/test-api-credentials', name: 'api.action.paypal.test-api-credentials', methods: ['POST'], defaults: ['_acl' => ['swag_paypal.viewer']])] + public function testApiCredentials(RequestDataBag $data): JsonResponse + { + $clientId = $data->getString('clientId'); + if (!$clientId) { + throw RoutingException::invalidRequestParameter('clientId'); + } + + $clientSecret = $data->getString('clientSecret'); + if (!$clientSecret) { + throw RoutingException::invalidRequestParameter('clientSecret'); + } + + $merchantPayerId = $data->get('merchantPayerId'); + if ($merchantPayerId !== null && !\is_string($merchantPayerId)) { + throw RoutingException::invalidRequestParameter('merchantPayerId'); + } + + $sandboxActive = $data->getBoolean('sandboxActive'); + + try { + /* @phpstan-ignore-next-line method will have additional method */ + $valid = $this->apiCredentialService->testApiCredentials($clientId, $clientSecret, $sandboxActive, $merchantPayerId); + } catch (PayPalApiException $error) { + $valid = false; + $errors = (new ErrorResponseFactory())->getErrorsFromException($error); + } + + return new JsonResponse([ + 'valid' => $valid, + 'errors' => $errors ?? [], + ]); + } + #[OA\Post( path: '/api/_action/paypal/get-api-credentials', operationId: 'getApiCredentials', @@ -153,4 +217,32 @@ public function getMerchantInformation(Request $request, Context $context): Json return new JsonResponse($response); } + + #[OA\Post( + path: '/api/_action/paypal/save-settings', + operationId: 'saveSettings', + tags: ['Admin Api', 'PayPal'], + responses: [new OA\Response( + response: Response::HTTP_OK, + description: 'Returns information about the saved settings', + content: new OA\JsonContent(type: 'object', additionalProperties: new OA\AdditionalProperties(ref: SettingsInformationStruct::class)) + )] + )] + #[Route(path: '/api/_action/paypal/save-settings', name: 'api.action.paypal.settings.save', methods: ['POST'], defaults: ['_acl' => ['swag_paypal.editor', 'system_config:update', 'system_config:create', 'system_config:delete']])] + public function saveSettings(RequestDataBag $data, Context $context): JsonResponse + { + $this->systemConfigValidator->validate($data->all(), $context); + + $information = []; + + /** + * @var string $salesChannel + * @var array $kvs + */ + foreach ($data->all() as $salesChannel => $kvs) { + $information[$salesChannel] = $this->settingsSaver->save($kvs, $salesChannel === 'null' ? null : $salesChannel); + } + + return new JsonResponse($information); + } } diff --git a/src/Setting/Struct/MerchantInformationStruct.php b/src/Setting/Struct/MerchantInformationStruct.php index 263dad610..358a88788 100644 --- a/src/Setting/Struct/MerchantInformationStruct.php +++ b/src/Setting/Struct/MerchantInformationStruct.php @@ -16,7 +16,7 @@ #[Package('checkout')] class MerchantInformationStruct extends Struct { - #[OA\Property(ref: MerchantIntegrations::class)] + #[OA\Property(ref: MerchantIntegrations::class, nullable: true)] protected ?MerchantIntegrations $merchantIntegrations; /** diff --git a/src/Setting/Struct/SettingsInformationStruct.php b/src/Setting/Struct/SettingsInformationStruct.php new file mode 100644 index 000000000..e7df3fb5d --- /dev/null +++ b/src/Setting/Struct/SettingsInformationStruct.php @@ -0,0 +1,91 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Swag\PayPal\Setting\Struct; + +use OpenApi\Attributes as OA; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\Framework\Struct\Struct; + +#[OA\Schema(schema: 'swag_paypal_setting_settings_information')] +#[Package('checkout')] +class SettingsInformationStruct extends Struct +{ + #[OA\Property(type: 'boolean')] + protected bool $sandboxCredentialsChanged = false; + + #[OA\Property(type: 'boolean', nullable: true)] + protected ?bool $sandboxCredentialsValid = null; + + #[OA\Property(type: 'boolean')] + protected bool $liveCredentialsChanged = false; + + #[OA\Property(type: 'boolean', nullable: true)] + protected ?bool $liveCredentialsValid = null; + + /** + * @var array + */ + #[OA\Property(type: 'array', items: new OA\Items(type: 'string'))] + protected array $webhookErrors = []; + + public function getSandboxCredentialsChanged(): bool + { + return $this->sandboxCredentialsChanged; + } + + public function setSandboxCredentialsChanged(bool $sandboxCredentialsChanged): void + { + $this->sandboxCredentialsChanged = $sandboxCredentialsChanged; + } + + public function getSandboxCredentialsValid(): ?bool + { + return $this->sandboxCredentialsValid; + } + + public function setSandboxCredentialsValid(?bool $sandboxCredentialsValid): void + { + $this->sandboxCredentialsValid = $sandboxCredentialsValid; + } + + public function getLiveCredentialsChanged(): bool + { + return $this->liveCredentialsChanged; + } + + public function setLiveCredentialsChanged(bool $liveCredentialsChanged): void + { + $this->liveCredentialsChanged = $liveCredentialsChanged; + } + + public function getLiveCredentialsValid(): ?bool + { + return $this->liveCredentialsValid; + } + + public function setLiveCredentialsValid(?bool $liveCredentialsValid): void + { + $this->liveCredentialsValid = $liveCredentialsValid; + } + + /** + * @return array + */ + public function getWebhookErrors(): array + { + return $this->webhookErrors; + } + + /** + * @param array $webhookErrors + */ + public function setWebhookErrors(array $webhookErrors): void + { + $this->webhookErrors = $webhookErrors; + } +} diff --git a/src/Webhook/Registration/WebhookSubscriber.php b/src/Webhook/Registration/WebhookSubscriber.php index c40fa17ea..560bfe836 100644 --- a/src/Webhook/Registration/WebhookSubscriber.php +++ b/src/Webhook/Registration/WebhookSubscriber.php @@ -9,12 +9,17 @@ use Psr\Log\LoggerInterface; use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent; +use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; use Shopware\Core\System\SalesChannel\SalesChannelEvents; +use Shopware\Core\System\SystemConfig\Event\BeforeSystemConfigMultipleChangedEvent; +use Shopware\Core\System\SystemConfig\Event\SystemConfigMultipleChangedEvent; use Shopware\Core\System\SystemConfig\SystemConfigService; +use Swag\PayPal\Setting\Service\SettingsSaver; use Swag\PayPal\Setting\Settings; use Swag\PayPal\Webhook\WebhookServiceInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * @internal @@ -22,26 +27,21 @@ #[Package('checkout')] class WebhookSubscriber implements EventSubscriberInterface { - private LoggerInterface $logger; - - private SystemConfigService $systemConfigService; - - private WebhookServiceInterface $webhookService; - public function __construct( - LoggerInterface $logger, - SystemConfigService $systemConfigService, - WebhookServiceInterface $webhookService, + private readonly LoggerInterface $logger, + private readonly SystemConfigService $systemConfigService, + private readonly WebhookServiceInterface $webhookService, + private readonly WebhookSystemConfigHelper $webhookSystemConfigHelper, + private readonly RequestStack $requestStack, ) { - $this->logger = $logger; - $this->systemConfigService = $systemConfigService; - $this->webhookService = $webhookService; } public static function getSubscribedEvents(): array { return [ SalesChannelEvents::SALES_CHANNEL_DELETED => 'removeSalesChannelWebhookConfiguration', + BeforeSystemConfigMultipleChangedEvent::class => 'checkWebhookBefore', + SystemConfigMultipleChangedEvent::class => 'checkWebhookAfter', ]; } @@ -60,4 +60,40 @@ public function removeSalesChannelWebhookConfiguration(EntityDeletedEvent $event } } } + + /** + * system-config should be written by {@see SettingsSaver} only, which checks the webhook on its own. + * Just in case new/changed credentials will be saved via the normal system config save route. + */ + public function checkWebhookBefore(BeforeSystemConfigMultipleChangedEvent $event): void + { + $routeName = (string) $this->requestStack->getMainRequest()?->attributes->getString('_route'); + + if (!\str_contains($routeName, 'api.action.core.save.system-config')) { + return; + } + + if (Feature::isActive('PAYPAL_SETTINGS_TWEAKS')) { + /** @var array> $config */ + $config = $event->getConfig(); + $this->webhookSystemConfigHelper->checkWebhookBefore($config); + } + } + + /** + * system-config should be written by {@see SettingsSaver} only, which checks the webhook on its own. + * Just in case new/changed credentials will be saved via the normal system config save route. + */ + public function checkWebhookAfter(SystemConfigMultipleChangedEvent $event): void + { + $routeName = (string) $this->requestStack->getMainRequest()?->attributes->getString('_route'); + + if (!\str_contains($routeName, 'api.action.core.save.system-config')) { + return; + } + + if (Feature::isActive('PAYPAL_SETTINGS_TWEAKS')) { + $this->webhookSystemConfigHelper->checkWebhookAfter(\array_keys($event->getConfig())); + } + } } diff --git a/src/Webhook/Registration/WebhookSystemConfigController.php b/src/Webhook/Registration/WebhookSystemConfigController.php index fa25e9ecd..65f60ef48 100644 --- a/src/Webhook/Registration/WebhookSystemConfigController.php +++ b/src/Webhook/Registration/WebhookSystemConfigController.php @@ -8,6 +8,7 @@ namespace Swag\PayPal\Webhook\Registration; use Shopware\Core\Framework\Context; +use Shopware\Core\Framework\Feature; use Shopware\Core\Framework\Log\Package; use Shopware\Core\System\SystemConfig\Api\SystemConfigController; use Shopware\Core\System\SystemConfig\Service\ConfigurationService; @@ -17,6 +18,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; +/** + * @deprecated tag:v10.0.0 - Will be removed. Use {@see SettingsController::saveSettings} instead + */ #[Package('checkout')] #[Route(defaults: ['_routeScope' => ['api']])] class WebhookSystemConfigController extends SystemConfigController @@ -40,6 +44,10 @@ public function __construct( public function saveConfiguration(Request $request): JsonResponse { + if (Feature::isActive('PAYPAL_SETTINGS_TWEAKS')) { + return parent::saveConfiguration($request); + } + $salesChannelId = $request->query->get('salesChannelId'); if (!\is_string($salesChannelId) || $salesChannelId === '') { $salesChannelId = 'null'; @@ -63,6 +71,10 @@ public function saveConfiguration(Request $request): JsonResponse public function batchSaveConfiguration(Request $request, Context $context): JsonResponse { + if (Feature::isActive('PAYPAL_SETTINGS_TWEAKS')) { + return parent::batchSaveConfiguration($request, $context); + } + /** @var array> $data */ $data = $request->request->all(); $errors = $this->webhookSystemConfigHelper->checkWebhookBefore($data); diff --git a/src/Webhook/Registration/WebhookSystemConfigHelper.php b/src/Webhook/Registration/WebhookSystemConfigHelper.php index 08a38d8e7..7f9ccaac8 100644 --- a/src/Webhook/Registration/WebhookSystemConfigHelper.php +++ b/src/Webhook/Registration/WebhookSystemConfigHelper.php @@ -93,7 +93,7 @@ public function checkWebhookBefore(array $newData): array } /** - * @param string[] $salesChannelIds + * @param array $salesChannelIds * * @return \Throwable[] */ diff --git a/tests/OpenAPISchemaTest.php b/tests/OpenAPISchemaTest.php index 48056f2ae..ac5d2c78a 100644 --- a/tests/OpenAPISchemaTest.php +++ b/tests/OpenAPISchemaTest.php @@ -58,6 +58,7 @@ class OpenAPISchemaTest extends TestCase public const IGNORED_LOG_MESSAGES = [ 'Required @OA\Info() not found', '$ref "#/components/schemas/" not found for @OA\Response() in \Swag\PayPal\Checkout\ExpressCheckout\SalesChannel\ExpressCategoryRoute->load()', + '$ref "#/components/schemas/" not found for @OA\Items() in \Swag\PayPal\Setting\SettingsController->testApiCredentials()', ]; private OpenApi $oa; diff --git a/tests/Setting/Service/SettingsSaverTest.php b/tests/Setting/Service/SettingsSaverTest.php new file mode 100644 index 000000000..66d1c123f --- /dev/null +++ b/tests/Setting/Service/SettingsSaverTest.php @@ -0,0 +1,431 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Swag\PayPal\Test\Setting\Service; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Shopware\Core\Framework\Log\Package; +use Shopware\Core\System\SystemConfig\SystemConfigService; +use Swag\PayPal\Setting\Service\ApiCredentialService; +use Swag\PayPal\Setting\Service\SettingsSaver; +use Swag\PayPal\Setting\Settings; +use Swag\PayPal\Webhook\Registration\WebhookSystemConfigHelper; + +/** + * @internal + */ +#[Package('checkout')] +class SettingsSaverTest extends TestCase +{ + private const VALID_LIVE_CREDENTIALS_1 = [ + Settings::CLIENT_ID => 'valid-client-id-1', + Settings::CLIENT_SECRET => 'valid-client-secret-1', + Settings::MERCHANT_PAYER_ID => 'valid-merchant-id-1', + ]; + + private const VALID_SANDBOX_CREDENTIALS_1 = [ + Settings::CLIENT_ID_SANDBOX => 'valid-client-id-sandbox-1', + Settings::CLIENT_SECRET_SANDBOX => 'valid-client-secret-sandbox-1', + Settings::MERCHANT_PAYER_ID_SANDBOX => 'valid-merchant-id-sandbox-1', + ]; + + private const VALID_LIVE_CREDENTIALS_2 = [ + Settings::CLIENT_ID => 'valid-client-id-2', + Settings::CLIENT_SECRET => 'valid-client-secret-2', + Settings::MERCHANT_PAYER_ID => 'valid-merchant-id-2', + ]; + + private const VALID_SANDBOX_CREDENTIALS_2 = [ + Settings::CLIENT_ID_SANDBOX => 'valid-client-id-sandbox-2', + Settings::CLIENT_SECRET_SANDBOX => 'valid-client-secret-sandbox-2', + Settings::MERCHANT_PAYER_ID_SANDBOX => 'valid-merchant-id-sandbox-2', + ]; + + private SystemConfigService&MockObject $systemConfigService; + + private ApiCredentialService&MockObject $apiCredentialService; + + private WebhookSystemConfigHelper&MockObject $webhookSystemConfigHelper; + + private SettingsSaver $settingsSaver; + + protected function setUp(): void + { + $this->systemConfigService = $this->createMock(SystemConfigService::class); + $this->apiCredentialService = $this->createMock(ApiCredentialService::class); + $this->webhookSystemConfigHelper = $this->createMock(WebhookSystemConfigHelper::class); + + $this->settingsSaver = new SettingsSaver( + $this->systemConfigService, + $this->apiCredentialService, + $this->webhookSystemConfigHelper + ); + } + + /** + * @param array $newSettings + * @param array> $oldSettings + */ + #[DataProvider('saveDataProvider')] + public function testSave( + array $newSettings, + array $oldSettings, + ?string $salesChannelId, + bool $liveChanged, + ?bool $liveValid, + bool $sandboxChanged, + ?bool $sandboxValid, + ): void { + $this->webhookSystemConfigHelper + ->expects(static::exactly((int) ($liveValid || $sandboxValid))) + ->method('checkWebhookBefore') + ->with([$salesChannelId => $newSettings]) + ->willReturn([]); + + $this->webhookSystemConfigHelper + ->expects($liveValid || $sandboxValid ? static::once() : static::never()) + ->method('checkWebhookAfter') + ->with([$salesChannelId]) + ->willReturn([]); + + $this->systemConfigService + ->expects(static::atLeast(6)) + ->method('get') + ->willReturnCallback(static function (string $key, ?string $salesChannelId) use ($oldSettings) { + return $oldSettings[$salesChannelId][$key] ?? null; + }); + + $this->systemConfigService + ->expects(static::once()) + ->method('setMultiple') + ->with($newSettings, $salesChannelId); + + $this->apiCredentialService + ->expects(static::exactly((int) $liveChanged + (int) $sandboxChanged)) + ->method('testApiCredentials') + ->willReturnCallback(static function (string $clientId, string $clientSecret, bool $sandbox, string $merchantId) use ($liveValid, $sandboxValid) { + $validExpected = $sandbox ? $sandboxValid : $liveValid; + $credentialsValid = \str_contains($clientId, 'valid') + && \str_contains($clientSecret, 'valid') + && \str_contains($merchantId, 'valid'); + + return $validExpected && $credentialsValid; + }); + + $information = $this->settingsSaver->save($newSettings, $salesChannelId); + + static::assertSame( + $liveChanged, + $information->getLiveCredentialsChanged(), + 'Expected live credentials to be ' . ($liveChanged ? 'changed' : 'not changed'), + ); + static::assertSame( + $liveValid, + $information->getLiveCredentialsValid(), + 'Expected live credentials to be ' . ($liveValid ? 'valid' : 'invalid'), + ); + static::assertSame( + $sandboxChanged, + $information->getSandboxCredentialsChanged(), + 'Expected sandbox credentials to be ' . ($sandboxChanged ? 'changed' : 'not changed'), + ); + static::assertSame( + $sandboxValid, + $information->getSandboxCredentialsValid(), + 'Expected sandbox credentials to be ' . ($sandboxValid ? 'valid' : 'invalid'), + ); + } + + public static function saveDataProvider(): \Generator + { + yield 'save simple settings' => [ + 'newSettings' => [Settings::BRAND_NAME => 'testBrandName'], + 'oldSettings' => [null => []], + 'salesChannelId' => null, + 'liveChanged' => false, + 'liveValid' => null, + 'sandboxChanged' => false, + 'sandboxValid' => null, + ]; + + yield 'added live credentials' => [ + 'newSettings' => self::VALID_LIVE_CREDENTIALS_1, + 'oldSettings' => [null => []], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => false, + 'sandboxValid' => null, + ]; + + yield 'added sandbox credentials' => [ + 'newSettings' => self::VALID_SANDBOX_CREDENTIALS_1, + 'oldSettings' => [null => []], + 'salesChannelId' => null, + 'liveChanged' => false, + 'liveValid' => null, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'added live + sandbox credentials' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ], + 'oldSettings' => [null => []], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'changed live credentials' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_2, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => false, + 'sandboxValid' => null, + ]; + + yield 'changed sandbox credentials' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_2, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => null, + 'liveChanged' => false, + 'liveValid' => null, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'changed live + sandbox credentials' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_2, + ...self::VALID_SANDBOX_CREDENTIALS_2, + ]], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'changed live credentials to be invalid' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + Settings::CLIENT_ID => null, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => false, + 'sandboxChanged' => false, + 'sandboxValid' => null, + ]; + + yield 'changed sandbox credentials to be invalid' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + Settings::CLIENT_ID_SANDBOX => null, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => null, + 'liveChanged' => false, + 'liveValid' => null, + 'sandboxChanged' => true, + 'sandboxValid' => false, + ]; + + yield 'changed live + sandbox credentials to be invalid' => [ + 'newSettings' => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + Settings::CLIENT_ID => null, + Settings::CLIENT_ID_SANDBOX => null, + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => null, + 'liveChanged' => true, + 'liveValid' => false, + 'sandboxChanged' => true, + 'sandboxValid' => false, + ]; + + yield 'added partial live credentials will inherit' => [ + 'newSettings' => [ + Settings::CLIENT_ID => 'valid-client-id', + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + Settings::CLIENT_ID => null, + ]], + 'salesChannelId' => 'sales-channel-id', + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => false, + 'sandboxValid' => null, + ]; + + yield 'added partial sandbox credentials will inherit' => [ + 'newSettings' => [ + Settings::CLIENT_ID_SANDBOX => 'valid-client-id-sandbox', + ], + 'oldSettings' => [null => [ + ...self::VALID_SANDBOX_CREDENTIALS_1, + Settings::CLIENT_ID_SANDBOX => null, + ]], + 'salesChannelId' => 'sales-channel-id', + 'liveChanged' => false, + 'liveValid' => null, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'added partial live + sandbox credentials will inherit' => [ + 'newSettings' => [ + Settings::CLIENT_ID => 'valid-client-id', + Settings::CLIENT_ID_SANDBOX => 'valid-client-id-sandbox', + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + Settings::CLIENT_ID => null, + Settings::CLIENT_ID_SANDBOX => null, + ]], + 'salesChannelId' => 'sales-channel-id', + 'liveChanged' => true, + 'liveValid' => true, + 'sandboxChanged' => true, + 'sandboxValid' => true, + ]; + + yield 'added partial invalid live + sandbox credentials will inherit' => [ + 'newSettings' => [ + Settings::CLIENT_ID => 'incorrect', + Settings::CLIENT_ID_SANDBOX => 'incorrect', + ], + 'oldSettings' => [null => [ + ...self::VALID_LIVE_CREDENTIALS_1, + ...self::VALID_SANDBOX_CREDENTIALS_1, + ]], + 'salesChannelId' => 'sales-channel-id', + 'liveChanged' => true, + 'liveValid' => false, + 'sandboxChanged' => true, + 'sandboxValid' => false, + ]; + } + + public function testWebhookErrors(): void + { + $salesChannelId = null; + $newSettings = self::VALID_LIVE_CREDENTIALS_1; + $errorsBefore = [new \RuntimeException('error-before')]; + $errorsAfter = [new \RuntimeException('error-after')]; + + $this->webhookSystemConfigHelper + ->expects(static::once()) + ->method('checkWebhookBefore') + ->with([$salesChannelId => $newSettings]) + ->willReturn($errorsBefore); + + $this->webhookSystemConfigHelper + ->expects(static::once()) + ->method('checkWebhookAfter') + ->with([$salesChannelId]) + ->willReturn($errorsAfter); + + $this->systemConfigService + ->method('get') + ->willReturn(null); + + $this->systemConfigService + ->expects(static::once()) + ->method('setMultiple') + ->with($newSettings, $salesChannelId); + + $this->apiCredentialService + ->expects(static::once()) + ->method('testApiCredentials') + ->willReturn(true); + + $information = $this->settingsSaver->save($newSettings, $salesChannelId); + + static::assertTrue($information->getLiveCredentialsChanged()); + static::assertTrue($information->getLiveCredentialsValid()); + static::assertEquals(['error-before', 'error-after'], $information->getWebhookErrors()); + } + + public function testApiCredentialsValidationError(): void + { + $salesChannelId = null; + $newSettings = self::VALID_LIVE_CREDENTIALS_1; + + $this->webhookSystemConfigHelper + ->expects(static::never()) + ->method('checkWebhookBefore') + ->with([$salesChannelId => $newSettings]) + ->willReturn([]); + + $this->webhookSystemConfigHelper + ->expects(static::never()) + ->method('checkWebhookAfter') + ->with([$salesChannelId]) + ->willReturn([]); + + $this->systemConfigService + ->method('get') + ->willReturn(null); + + $this->systemConfigService + ->expects(static::once()) + ->method('setMultiple') + ->with($newSettings, $salesChannelId); + + $this->apiCredentialService + ->expects(static::once()) + ->method('testApiCredentials') + ->willThrowException(new \RuntimeException('error')); + + $information = $this->settingsSaver->save($newSettings, $salesChannelId); + + static::assertTrue($information->getLiveCredentialsChanged()); + static::assertFalse($information->getLiveCredentialsValid()); + } +} diff --git a/tests/Setting/SettingsControllerTest.php b/tests/Setting/SettingsControllerTest.php index 7b66cbe5b..31b1d4298 100644 --- a/tests/Setting/SettingsControllerTest.php +++ b/tests/Setting/SettingsControllerTest.php @@ -11,6 +11,7 @@ use Psr\Log\NullLogger; use Shopware\Core\Framework\Log\Package; use Shopware\Core\Framework\Test\TestCaseBase\IntegrationTestBehaviour; +use Shopware\Core\System\SystemConfig\Validation\SystemConfigValidator; use Swag\PayPal\RestApi\Exception\PayPalApiException; use Swag\PayPal\RestApi\V1\Resource\CredentialsResource; use Swag\PayPal\RestApi\V1\Resource\MerchantIntegrationsResource; @@ -19,6 +20,7 @@ use Swag\PayPal\Setting\Service\ApiCredentialService; use Swag\PayPal\Setting\Service\CredentialsUtil; use Swag\PayPal\Setting\Service\MerchantIntegrationsService; +use Swag\PayPal\Setting\Service\SettingsSaver; use Swag\PayPal\Setting\Service\SettingsValidationService; use Swag\PayPal\Setting\SettingsController; use Swag\PayPal\Test\Helper\ConstantsForTesting; @@ -28,6 +30,7 @@ use Swag\PayPal\Test\Mock\PayPal\Client\PayPalClientFactoryMock; use Swag\PayPal\Test\Mock\PayPal\Client\TokenClientFactoryMock; use Swag\PayPal\Util\Lifecycle\Method\PaymentMethodDataRegistry; +use Swag\PayPal\Webhook\Registration\WebhookSystemConfigHelper; use Symfony\Component\HttpFoundation\Request; /** @@ -79,28 +82,35 @@ private function createApiValidationController(): SettingsController { $logger = new NullLogger(); $systemConfigService = $this->createDefaultSystemConfig(); - - return new SettingsController( - new ApiCredentialService( - new CredentialsResource( - new TokenClientFactoryMock($logger), - new CredentialsClientFactoryMock($logger), - new TokenValidator() - ), + $apiCredentialsService = new ApiCredentialService( + new CredentialsResource( new TokenClientFactoryMock($logger), - new TokenValidator(), - new CredentialProvider( - new SettingsValidationService($systemConfigService, $logger), - $systemConfigService, - new CredentialsUtil($systemConfigService) - ), - $logger, + new CredentialsClientFactoryMock($logger), + new TokenValidator() ), + new TokenClientFactoryMock($logger), + new TokenValidator(), + new CredentialProvider( + new SettingsValidationService($systemConfigService, $logger), + $systemConfigService, + new CredentialsUtil($systemConfigService) + ), + $logger, + ); + + return new SettingsController( + $apiCredentialsService, new MerchantIntegrationsService( new MerchantIntegrationsResource(new PayPalClientFactoryMock($logger)), new CredentialsUtil($systemConfigService), $this->getContainer()->get(PaymentMethodDataRegistry::class), new PayPalClientFactoryMock($logger) + ), + $this->getContainer()->get(SystemConfigValidator::class), + new SettingsSaver( + $systemConfigService, + $apiCredentialsService, + $this->createMock(WebhookSystemConfigHelper::class), ) ); } diff --git a/tests/Webhook/Registration/WebhookSubscriberTest.php b/tests/Webhook/Registration/WebhookSubscriberTest.php index 67ed81d5c..5f8898b70 100644 --- a/tests/Webhook/Registration/WebhookSubscriberTest.php +++ b/tests/Webhook/Registration/WebhookSubscriberTest.php @@ -16,6 +16,8 @@ use Shopware\Core\Framework\Log\Package; use Shopware\Core\System\SalesChannel\SalesChannelDefinition; use Shopware\Core\System\SalesChannel\SalesChannelEvents; +use Shopware\Core\System\SystemConfig\Event\BeforeSystemConfigMultipleChangedEvent; +use Shopware\Core\System\SystemConfig\Event\SystemConfigMultipleChangedEvent; use Shopware\Core\System\SystemConfig\SystemConfigService; use Shopware\Core\Test\TestDefaults; use Swag\PayPal\RestApi\V1\Resource\WebhookResource; @@ -24,8 +26,10 @@ use Swag\PayPal\Test\Mock\PayPal\Client\PayPalClientFactoryMock; use Swag\PayPal\Test\Mock\Setting\Service\SystemConfigServiceMock; use Swag\PayPal\Webhook\Registration\WebhookSubscriber; +use Swag\PayPal\Webhook\Registration\WebhookSystemConfigHelper; use Swag\PayPal\Webhook\WebhookRegistry; use Swag\PayPal\Webhook\WebhookService; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\RouterInterface; /** @@ -73,6 +77,8 @@ public function testSubscribedEvents(): void { static::assertEqualsCanonicalizing([ SalesChannelEvents::SALES_CHANNEL_DELETED => 'removeSalesChannelWebhookConfiguration', + BeforeSystemConfigMultipleChangedEvent::class => 'checkWebhookBefore', + SystemConfigMultipleChangedEvent::class => 'checkWebhookAfter', ], WebhookSubscriber::getSubscribedEvents()); } @@ -98,7 +104,9 @@ private function createWebhookSubscriber(array $configuration): WebhookSubscribe return new WebhookSubscriber( new NullLogger(), $this->systemConfigService, - $webhookService + $webhookService, + $this->createMock(WebhookSystemConfigHelper::class), + new RequestStack(), ); } diff --git a/tests/Webhook/Registration/WebhookSystemConfigControllerTest.php b/tests/Webhook/Registration/WebhookSystemConfigControllerTest.php index cbbc21d96..cca2dffd1 100644 --- a/tests/Webhook/Registration/WebhookSystemConfigControllerTest.php +++ b/tests/Webhook/Registration/WebhookSystemConfigControllerTest.php @@ -16,6 +16,7 @@ use Shopware\Core\System\SystemConfig\Service\ConfigurationService; use Shopware\Core\System\SystemConfig\SystemConfigService; use Shopware\Core\System\SystemConfig\Validation\SystemConfigValidator; +use Shopware\Core\Test\Annotation\DisabledFeatures; use Shopware\Core\Test\TestDefaults; use Swag\PayPal\Setting\Service\SettingsValidationService; use Swag\PayPal\Setting\Settings; @@ -28,6 +29,7 @@ /** * @internal */ +#[DisabledFeatures(features: ['PAYPAL_SETTINGS_TWEAKS'])] #[Package('checkout')] class WebhookSystemConfigControllerTest extends TestCase {