diff --git a/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php b/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php new file mode 100644 index 0000000000000..fd94e18e2cd94 --- /dev/null +++ b/app/code/Magento/Braintree/Model/Ui/Adminhtml/PayPal/TokenUiComponentProvider.php @@ -0,0 +1,84 @@ +componentFactory = $componentFactory; + $this->urlBuilder = $urlBuilder; + $this->config = $config; + } + + /** + * @inheritdoc + */ + public function getComponentForToken(PaymentTokenInterface $paymentToken) + { + $data = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + $data['icon'] = $this->config->getPayPalIcon(); + $component = $this->componentFactory->create( + [ + 'config' => [ + 'code' => PayPalConfigProvider::PAYPAL_VAULT_CODE, + 'nonceUrl' => $this->getNonceRetrieveUrl(), + TokenUiComponentProviderInterface::COMPONENT_DETAILS => $data, + TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash(), + 'template' => 'Magento_Braintree::form/paypal/vault.phtml' + ], + 'name' => Template::class + ] + ); + + return $component; + } + + /** + * Get url to retrieve payment method nonce + * @return string + */ + private function getNonceRetrieveUrl() + { + return $this->urlBuilder->getUrl(ConfigProvider::CODE . '/payment/getnonce', ['_secure' => true]); + } +} diff --git a/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php b/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php index 6cfc96ea23d0d..420b8365b3ea4 100644 --- a/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php +++ b/app/code/Magento/Braintree/Model/Ui/Adminhtml/TokenUiComponentProvider.php @@ -49,6 +49,7 @@ public function getComponentForToken(PaymentTokenInterface $paymentToken) $component = $this->componentFactory->create( [ 'config' => [ + 'code' => ConfigProvider::CC_VAULT_CODE, 'nonceUrl' => $this->getNonceRetrieveUrl(), TokenUiComponentProviderInterface::COMPONENT_DETAILS => $data, TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash(), diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php new file mode 100644 index 0000000000000..bdc39cbc5b868 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/PayPal/TokenUiComponentProviderTest.php @@ -0,0 +1,114 @@ +componentFactory = $this->getMockBuilder(TokenUiComponentInterfaceFactory::class) + ->disableOriginalConstructor() + ->setMethods(['create']) + ->getMock(); + + $this->urlBuilder = $this->getMock(UrlInterface::class); + + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->setMethods(['getPayPalIcon']) + ->getMock(); + + $this->tokenUiComponentProvider = new TokenUiComponentProvider( + $this->componentFactory, + $this->urlBuilder, + $this->config + ); + } + + /** + * @covers \Magento\Braintree\Model\Ui\Adminhtml\PayPal\TokenUiComponentProvider::getComponentForToken + */ + public function testGetComponentForToken() + { + $nonceUrl = 'https://payment/adminhtml/nonce/url'; + $payerEmail = 'john.doe@test.com'; + $icon = [ + 'url' => 'https://payment/adminhtml/icon.png', + 'width' => 48, + 'height' => 32 + ]; + + $expected = [ + 'code' => 'vault', + 'nonceUrl' => $nonceUrl, + 'details' => [ + 'payerEmail' => $payerEmail, + 'icon' => $icon + ], + 'template' => 'vault.phtml' + ]; + + $this->config->expects(static::once()) + ->method('getPayPalIcon') + ->willReturn($icon); + + $paymentToken = $this->getMock(PaymentTokenInterface::class); + $paymentToken->expects(static::once()) + ->method('getTokenDetails') + ->willReturn('{"payerEmail":" ' . $payerEmail . '"}'); + $paymentToken->expects(static::once()) + ->method('getPublicHash') + ->willReturn('cmk32dl21l'); + + $this->urlBuilder->expects(static::once()) + ->method('getUrl') + ->willReturn($nonceUrl); + + $tokenComponent = $this->getMock(TokenUiComponentInterface::class); + $tokenComponent->expects(static::once()) + ->method('getConfig') + ->willReturn($expected); + + $this->componentFactory->expects(static::once()) + ->method('create') + ->willReturn($tokenComponent); + + $component = $this->tokenUiComponentProvider->getComponentForToken($paymentToken); + static::assertEquals($tokenComponent, $component); + static::assertEquals($expected, $component->getConfig()); + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php index d1665c71804cf..f159136cf4c46 100644 --- a/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Model/Ui/Adminhtml/TokenUiComponentProviderTest.php @@ -7,10 +7,10 @@ use Magento\Braintree\Model\Ui\Adminhtml\TokenUiComponentProvider; use Magento\Framework\UrlInterface; -use Magento\Framework\View\Element\Template; use Magento\Vault\Api\Data\PaymentTokenInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterfaceFactory; +use PHPUnit_Framework_MockObject_MockObject as MockObject; /** * Class TokenUiComponentProviderTest @@ -19,12 +19,12 @@ class TokenUiComponentProviderTest extends \PHPUnit_Framework_TestCase { /** - * @var TokenUiComponentInterfaceFactory|\PHPUnit_Framework_MockObject_MockObject + * @var TokenUiComponentInterfaceFactory|MockObject */ private $componentFactory; /** - * @var UrlInterface|\PHPUnit_Framework_MockObject_MockObject + * @var UrlInterface|MockObject */ private $urlBuilder; @@ -59,6 +59,7 @@ public function testGetComponentForToken() $expirationDate = '12/2015'; $expected = [ + 'code' => 'vault', 'nonceUrl' => $nonceUrl, 'details' => [ 'type' => $type, diff --git a/app/code/Magento/Braintree/etc/adminhtml/di.xml b/app/code/Magento/Braintree/etc/adminhtml/di.xml index f252b977f20bd..d154aabbb01b5 100644 --- a/app/code/Magento/Braintree/etc/adminhtml/di.xml +++ b/app/code/Magento/Braintree/etc/adminhtml/di.xml @@ -47,6 +47,7 @@ Magento\Braintree\Model\Ui\Adminhtml\TokenUiComponentProvider + Magento\Braintree\Model\Ui\Adminhtml\PayPal\TokenUiComponentProvider diff --git a/app/code/Magento/Braintree/etc/config.xml b/app/code/Magento/Braintree/etc/config.xml index 095a8419c8529..bf19324ae7a02 100644 --- a/app/code/Magento/Braintree/etc/config.xml +++ b/app/code/Magento/Braintree/etc/config.xml @@ -71,7 +71,8 @@ BraintreePayPalVaultFacade - Vault Token (Braintree PayPal) + Stored Accounts (Braintree PayPal) + 1 diff --git a/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml b/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml index 571c5ededeb99..5e4f36e1c1fb4 100644 --- a/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml +++ b/app/code/Magento/Braintree/view/adminhtml/layout/sales_order_create_index.xml @@ -18,6 +18,10 @@ braintree_cc_vault Magento_Vault::form/vault.phtml + + braintree_paypal_vault + Magento_Vault::form/vault.phtml + braintree_cc_vault Magento_Vault::form/vault.phtml + + braintree_paypal_vault + Magento_Vault::form/vault.phtml + \ No newline at end of file diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml new file mode 100644 index 0000000000000..22930bbc65666 --- /dev/null +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/paypal/vault.phtml @@ -0,0 +1,30 @@ +getData(TokenUiComponentProviderInterface::COMPONENT_DETAILS); +$icon = $details['icon']; +$id = $block->escapeHtml($block->getData('id')); +?> +
", + "nonceUrl": "escapeUrl($block->getData('nonceUrl')); ?>" + } + }' id="payment_" class="admin__field"> +
+ + + escapeHtml($details['payerEmail']); ?> +
+
diff --git a/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml b/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml index 3811461884725..001422d4bf911 100644 --- a/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml +++ b/app/code/Magento/Braintree/view/adminhtml/templates/form/vault.phtml @@ -7,7 +7,7 @@ use Magento\Vault\Model\Ui\TokenUiComponentProviderInterface; // @codingStandardsIgnoreFile /** @var \Magento\Framework\View\Element\Template $block */ -$details = $block->getData('details'); +$details = $block->getData(TokenUiComponentProviderInterface::COMPONENT_DETAILS); $icon = $block->getData('icons')[$details['type']]; $id = $block->escapeHtml($block->getData('id')); ?> @@ -15,6 +15,7 @@ $id = $block->escapeHtml($block->getData('id')); "Magento_Braintree/js/vault": { "container": "payment_", "publicHash": "escapeHtml($block->getData(TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH)); ?>", + "code": "escapeHtml($block->getData('code')); ?>", "nonceUrl": "escapeUrl($block->getData('nonceUrl')); ?>" } }' id="payment_" class="admin__field"> diff --git a/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js b/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js index fcff173e7fcd4..ea832acb537e0 100644 --- a/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js +++ b/app/code/Magento/Braintree/view/adminhtml/web/js/vault.js @@ -14,7 +14,8 @@ define([ return Class.extend({ defaults: { $selector: null, - selector: 'edit_form' + selector: 'edit_form', + $container: null }, /** @@ -25,17 +26,18 @@ define([ var self = this; self.$selector = $('#' + self.selector); + self.$container = $('#' + self.container); self.$selector.on( 'setVaultNotActive', function () { - self.$selector.off('submitOrder.braintree_vault'); + self.$selector.off('submitOrder.' + self.getCode()); } ); - this._super(); + self._super(); - this.initEventHandlers(); + self.initEventHandlers(); - return this; + return self; }, /** @@ -43,14 +45,14 @@ define([ * @returns {String} */ getCode: function () { - return 'braintree'; + return this.code; }, /** * Init event handlers */ initEventHandlers: function () { - $('#' + this.container).find('[name="payment[token_switcher]"]') + $(this.$container).find('[name="payment[token_switcher]"]') .on('click', this.selectPaymentMethod.bind(this)); }, @@ -66,7 +68,7 @@ define([ * Enable form event listeners */ enableEventListeners: function () { - this.$selector.on('submitOrder.braintree_vault', this.submitOrder.bind(this)); + this.$selector.on('submitOrder.' + this.getCode(), this.submitOrder.bind(this)); }, /** @@ -129,7 +131,7 @@ define([ this.createPublicHashSelector(); this.$selector.find('[name="payment[public_hash]"]').val(this.publicHash); - this.$selector.find('#braintree_nonce').val(nonce); + this.$container.find('#' + this.getNonceSelectorName()).val(nonce); }, /** @@ -138,16 +140,16 @@ define([ createPublicHashSelector: function () { var $input; - if (this.$selector.find('#braintree_nonce').size() === 0) { + if (this.$container.find('#' + this.getNonceSelectorName()).size() === 0) { $input = $('').attr( { type: 'hidden', - id: 'braintree_nonce', + id: this.getNonceSelectorName(), name: 'payment[payment_method_nonce]' } ); - $input.appendTo(this.$selector); + $input.appendTo(this.$container); $input.prop('disabled', false); } }, @@ -160,6 +162,14 @@ define([ alert({ content: message }); + }, + + /** + * Get selector name for nonce input + * @returns {String} + */ + getNonceSelectorName: function () { + return 'nonce_' + this.getCode(); } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js new file mode 100644 index 0000000000000..4236a215d7359 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/method-group.js @@ -0,0 +1,31 @@ +/** + * Copyright © 2016 Magento. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'mage/translate' +], function (Element, $t) { + 'use strict'; + + var DEFAULT_GROUP_ALIAS = 'default'; + + return Element.extend({ + defaults: { + alias: DEFAULT_GROUP_ALIAS, + title: $t('Payment Method'), + sortOrder: 100, + displayArea: 'payment-methods-items-${ $.alias }' + }, + + /** + * Checks if group instance is default + * + * @returns {Boolean} + */ + isDefault: function () { + return this.alias === DEFAULT_GROUP_ALIAS; + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js index 18f6b8479c229..918d305ee031b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/payment/list.js @@ -10,14 +10,22 @@ define([ 'Magento_Checkout/js/model/payment/method-list', 'Magento_Checkout/js/model/payment/renderer-list', 'uiLayout', - 'Magento_Checkout/js/model/checkout-data-resolver' -], function (_, ko, utils, Component, paymentMethods, rendererList, layout, checkoutDataResolver) { + 'Magento_Checkout/js/model/checkout-data-resolver', + 'mage/translate', + 'uiRegistry' +], function (_, ko, utils, Component, paymentMethods, rendererList, layout, checkoutDataResolver, $t, registry) { 'use strict'; return Component.extend({ defaults: { template: 'Magento_Checkout/payment-methods/list', - visible: paymentMethods().length > 0 + visible: paymentMethods().length > 0, + configDefaultGroup: { + name: 'methodGroup', + component: 'Magento_Checkout/js/model/payment/method-group' + }, + paymentGroupsList: [], + defaultGroupTitle: $t('Select a new payment method') }, /** @@ -26,7 +34,7 @@ define([ * @returns {Component} Chainable. */ initialize: function () { - this._super().initChildren(); + this._super().initDefaulGroup().initChildren(); paymentMethods.subscribe( function (changes) { checkoutDataResolver.resolvePaymentMethod(); @@ -47,6 +55,27 @@ define([ return this; }, + /** @inheritdoc */ + initObservable: function () { + this._super(). + observe(['paymentGroupsList']); + + return this; + }, + + /** + * Creates default group + * + * @returns {Component} Chainable. + */ + initDefaulGroup: function() { + layout([ + this.configDefaultGroup + ]); + + return this; + }, + /** * Create renders for child payment methods. * @@ -77,7 +106,7 @@ define([ rendererTemplate = { parent: '${ $.$data.parentName }', name: '${ $.$data.name }', - displayArea: 'payment-method-items', + displayArea: payment.displayArea, component: payment.component }; rendererComponent = utils.template(rendererTemplate, templateData); @@ -95,49 +124,105 @@ define([ * @param {Object} paymentMethodData */ createRenderer: function (paymentMethodData) { - var isRendererForMethod = false; + var isRendererForMethod = false, + currentGroup; + + registry.get(this.configDefaultGroup.name, function (defaultGroup) { + _.each(rendererList(), function (renderer) { - _.find(rendererList(), function (renderer) { + if (renderer.hasOwnProperty('typeComparatorCallback') && + typeof renderer.typeComparatorCallback == 'function' + ) { + isRendererForMethod = renderer.typeComparatorCallback(renderer.type, paymentMethodData.method); + } else { + isRendererForMethod = renderer.type === paymentMethodData.method; + } - if (renderer.hasOwnProperty('typeComparatorCallback') && - typeof renderer.typeComparatorCallback == 'function' - ) { - isRendererForMethod = renderer.typeComparatorCallback(renderer.type, paymentMethodData.method); - } else { - isRendererForMethod = renderer.type === paymentMethodData.method; - } + if (isRendererForMethod) { + currentGroup = renderer.group ? renderer.group : defaultGroup; - if (isRendererForMethod) { - layout( - [ + this.collectPaymentGroups(currentGroup); + + layout([ this.createComponent( { config: renderer.config, component: renderer.component, name: renderer.type, method: paymentMethodData.method, - item: paymentMethodData + item: paymentMethodData, + displayArea: currentGroup.displayArea } - ) - ] - ); - } + )]); + } + }.bind(this)); }.bind(this)); }, + /** + * Collects unique groups of available payment methods + * + * @param {Object} group + */ + collectPaymentGroups: function (group) { + var groupsList = this.paymentGroupsList(), + isGroupExists = _.some(groupsList, function (existsGroup) { + return existsGroup.alias === group.alias; + }); + + if (!isGroupExists) { + groupsList.push(group); + groupsList = _.sortBy(groupsList, function (existsGroup) { + return existsGroup.sortOrder; + }); + this.paymentGroupsList(groupsList); + } + }, + + /** + * Returns payment group title + * + * @param {Object} group + * @returns {String} + */ + getGroupTitle: function (group) { + var title = group().title; + + if (group().isDefault() && this.paymentGroupsList().length > 1) { + title = this.defaultGroupTitle; + } + + return title + ':'; + }, + + /** + * Checks if at least one payment method available + * + * @returns {String} + */ + isPaymentMethodsAvailable: function () { + return _.some(this.paymentGroupsList(), function (group) { + return this.getRegion(group.displayArea)().length; + }, this); + }, + /** * Remove view renderer. * * @param {String} paymentMethodCode */ removeRenderer: function (paymentMethodCode) { - var items = this.getRegion('payment-method-items'); + var items; + + _.each(this.paymentGroupsList(), function (group) { + items = this.getRegion(group.displayArea); - _.find(items(), function (value) { - if (value.item.method.indexOf(paymentMethodCode) === 0) { - value.disposeSubscriptions(); - value.destroy(); - } + _.find(items(), function (value) { + if (value.item.method.indexOf(paymentMethodCode) === 0) { + value.disposeSubscriptions(); + value.destroy(); + } + }); }, this); } }); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/payment-methods/list.html b/app/code/Magento/Checkout/view/frontend/web/template/payment-methods/list.html index 66769f37386d6..f6c41c62eeca6 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/payment-methods/list.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/payment-methods/list.html @@ -4,9 +4,19 @@ * See COPYING.txt for license details. */ --> -
- - - +
+
+
+
+ +
+
+
-
diff --git a/app/code/Magento/Checkout/view/frontend/web/template/payment.html b/app/code/Magento/Checkout/view/frontend/web/template/payment.html index 292ece611e6b6..46467839da3fb 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/payment.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/payment.html @@ -5,7 +5,6 @@ */ -->