From 3bc8d4182dbf888c4e61b721355cba575eab924e Mon Sep 17 00:00:00 2001 From: keeri Date: Mon, 27 Jun 2022 20:04:50 +0000 Subject: [PATCH] Add merchant mode (#556) * merchant mode (work in progress) stuff left to do: payment successful state, footer state when a different amount is received with an option to create new qr with the difference (if amount is less than requested) or an option to consider payment as paid (if amount is more than requested, as it might be a different customer) * fix header not being visually centered * merchant mode prompts and style improvements "transaction complete" state still left to do * merchant mode prompts style tweaks, fix account select alignment * merchant mode payment complete state * replace tiny icon with a big button * unused variable --- .../components/receive/receive.component.css | 133 +++++++- .../components/receive/receive.component.html | 293 +++++++++++++++++- .../components/receive/receive.component.ts | 165 ++++++++++ src/less/components/dark-mode.less | 10 + src/less/nault-theme.less | 34 +- 5 files changed, 625 insertions(+), 10 deletions(-) diff --git a/src/app/components/receive/receive.component.css b/src/app/components/receive/receive.component.css index 8a9eb2c..2845fc9 100644 --- a/src/app/components/receive/receive.component.css +++ b/src/app/components/receive/receive.component.css @@ -1,3 +1,18 @@ +.merchant-mode-text-full { + display: none; +} +.merchant-mode-text-short { + display: inline-block; +} +@media (min-width: 460px) { + .merchant-mode-text-full { + display: inline-block; + } + .merchant-mode-text-short { + display: none; + } +} + .label-block { margin-left: 10px; margin-bottom: 10px; @@ -63,9 +78,6 @@ max-width: 280px; margin: 25px auto 0 auto; } -.copy-address-button { - margin-top: 25px; -} @media (max-width: 959px) { .qr-code { max-width: 250px; @@ -161,4 +173,117 @@ transform: scale(0); opacity: 1; } -} \ No newline at end of file +} + +.merchant-mode-overlay { + background-color: #FFF; + z-index: 1050; /* above notifications */ + padding: 15px !important; +} + +.merchant-mode-contents { + box-sizing: border-box; + height: 100%; + padding: 20px; +} +@media (max-width: 699px) { + .merchant-mode-contents { + padding: 10px; + } +} + +.merchant-mode-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 25px; +} +@media (max-width: 699px) { + .merchant-mode-header { + flex-direction: column; + padding-left: 0; + } +} +@media (min-width: 1200px) { + .merchant-mode-header { + position: fixed; + width: calc(100% - 86px); + } +} + +.merchant-mode-exit .label { + font-size: 1.2em; +} + +.merchant-mode-logo { + width: 200px; + height: 85px; + background: url('../../../assets/img/nault-logo.svg'); + background-size: contain; + background-repeat: no-repeat; + flex-shrink: 0; +} + +.merchant-mode-exit { + cursor: pointer; + padding: 6px 23px 8px 20px; + border: 2px solid transparent; + border-radius: 50px; + transition: border-color 100ms ease-in-out; +} +.merchant-mode-exit:hover { + border-color: #676686AA; +} + +.merchant-centered-container { + padding-bottom: 170px; + max-width: 500px; +} +@media (min-width: 1200px) { + .merchant-centered-container { + padding-bottom: 0; + } +} + +.merchant-mode-icon-qr-code { + width: 19px; + height: 19px; + margin-right: 10px; + background-color: currentcolor; + -webkit-mask-image: url('assets/img/qr-code.svg'); + mask-image: url('assets/img/qr-code.svg'); +} + +.merchant-mode-qr-code { + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + width: 290px; + height: 290px; + border-radius: 20px; + border: 2px solid #19191B; + background-color: #FFF; +} + +.merchant-mode-amount-integer, +.merchant-mode-amount-fractional, +.merchant-mode-currency-name { + font-family: 'Montserrat', Arial, Helvetica, sans-serif; + font-size: 34px; +} + +.merchant-mode-amount-integer { + font-weight: 700; +} + +.merchant-mode-amount-fractional { + font-weight: 500; +} + +.merchant-mode-currency-name { + font-weight: 400; +} +.merchant-mode-currency-margin { + margin-left: 10px; +} diff --git a/src/app/components/receive/receive.component.html b/src/app/components/receive/receive.component.html index 0826098..74ad4e3 100644 --- a/src/app/components/receive/receive.component.html +++ b/src/app/components/receive/receive.component.html @@ -1,7 +1,24 @@
+ + + + -

Receive Nano

+
+

Receive Nano

+
+ +
+
@@ -53,14 +70,15 @@
-
-
+
+
-
+
+
+ +
@@ -281,3 +303,264 @@
+
+
+
+ +
+ + {{ inMerchantModeQR ? 'Cancel Payment' : 'Exit Merchant Mode' }} +
+
+
+ +
+
+

Select the destination account

+ +
+
+
+ +
+ + +
+ + + +
+
+

Enter the requested amount

+ + +
+
+
+ +

Send

+

+ XNO +

+

+ {{ merchantModeRawRequestedQR | rai: 'mnano,true' | amountsplit: 0 }} + {{ merchantModeRawRequestedQR | rai: 'mnano,true' | amountsplit: 1 }} + XNO +

+

to

+ +
+ QR code +
+
+
+
+

+ Received +

+

+ Received more than requested +

+

+ Received less than requested +

+

+ {{ prompt.amountRaw | rai: 'mnano,true' | amountsplit: 0 }} + {{ prompt.amountRaw | rai: 'mnano,true' | amountsplit: 1 }} + XNO +

+
+ +{{ prompt.amountHiddenRaw.toString(10) }} raw +
+ + + +
+
+ +

Awaiting payment...

+ +
+
+ + Cancel Payment +
+
+ +

Received

+

+ {{ merchantModeRawReceivedTotal | rai: 'mnano,true' | amountsplit: 0 }} + {{ merchantModeRawReceivedTotal | rai: 'mnano,true' | amountsplit: 1 }} + XNO +

+
+ +{{ merchantModeRawReceivedTotalHiddenRaw.toString(10) }} raw +
+ +

+ +

+
    +
  • + {{ hash }} +
  • +
+
+ + New Payment +
+
+
+
+
diff --git a/src/app/components/receive/receive.component.ts b/src/app/components/receive/receive.component.ts index 352f859..1606cb6 100644 --- a/src/app/components/receive/receive.component.ts +++ b/src/app/components/receive/receive.component.ts @@ -29,6 +29,7 @@ export class ReceiveComponent implements OnInit, OnDestroy { timeoutIdClearingRecentlyCopiedState: any = null; mobileTransactionMenuModal: any = null; + merchantModeModal: any = null; mobileTransactionData: any = null; selectedAccountAddressBookName = ''; @@ -48,6 +49,17 @@ export class ReceiveComponent implements OnInit, OnDestroy { validFiat = true; qrSuccessClass = ''; + inMerchantMode = false; + inMerchantModeQR = false; + inMerchantModePaymentComplete = false; + merchantModeRawRequestedQR: BigNumber = null; + merchantModeRawRequestedTotal: BigNumber = null; + merchantModeRawReceivedTotal: BigNumber = null; + merchantModeRawReceivedTotalHiddenRaw: BigNumber = null; + merchantModeSeenBlockHashes = {}; + merchantModePrompts = []; + merchantModeTransactionHashes = []; + routerSub = null; constructor( @@ -67,12 +79,17 @@ export class ReceiveComponent implements OnInit, OnDestroy { async ngOnInit() { const UIkit = window['UIkit']; + const mobileTransactionMenuModal = UIkit.modal('#mobile-transaction-menu-modal'); this.mobileTransactionMenuModal = mobileTransactionMenuModal; + const merchantModeModal = UIkit.modal('#merchant-mode-modal'); + this.merchantModeModal = merchantModeModal; + this.routerSub = this.route.events.subscribe(event => { if (event instanceof ChildActivationEnd) { this.mobileTransactionMenuModal.hide(); + this.merchantModeModal.hide(); } }); @@ -110,12 +127,16 @@ export class ReceiveComponent implements OnInit, OnDestroy { this.showQrConfirmation(); setTimeout(() => this.resetAmount(), 500); } + if ( (this.inMerchantModeQR === true) && (transaction.block.link_as_account === this.qrAccount) ) { + this.onMerchantModeReceiveTransaction(transaction); + } } }); } ngOnDestroy() { this.mobileTransactionMenuModal.hide(); + this.merchantModeModal.hide(); if (this.routerSub) { this.routerSub.unsubscribe(); } @@ -164,6 +185,14 @@ export class ReceiveComponent implements OnInit, OnDestroy { // Blocks for selected account this.pendingBlocksForSelectedAccount = this.pendingBlocks.filter(block => (block.destination === selectedAccountID)); + + if (this.inMerchantModeQR === true) { + this.pendingBlocksForSelectedAccount.forEach( + (pendingBlock) => { + this.onMerchantModeReceiveTransaction(pendingBlock); + } + ) + } } showMobileMenuForTransaction(transaction) { @@ -364,4 +393,140 @@ export class ReceiveComponent implements OnInit, OnDestroy { return new BigNumber(value); } + unsetSelectedAccount() { + this.pendingAccountModel = '0'; + this.onSelectedAccountChange(this.pendingAccountModel); + } + + getRawAmountWithoutTinyRaws(rawAmountWithTinyRaws) { + const tinyRaws = + rawAmountWithTinyRaws.mod(this.nano); + + return rawAmountWithTinyRaws.minus(tinyRaws); + } + + merchantModeResetState() { + this.unsetSelectedAccount(); + this.resetAmount(); + + this.inMerchantModeQR = false; + this.inMerchantModePaymentComplete = false; + } + + merchantModeEnable() { + this.merchantModeResetState(); + + this.inMerchantMode = true; + this.merchantModeModal.show(); + } + + merchantModeDisable() { + this.inMerchantMode = false; + this.inMerchantModeQR = false; + this.inMerchantModePaymentComplete = false; + this.merchantModeModal.hide(); + } + + merchantModeShowQR() { + const isRequestingAnyAmount = (this.validNano === false || Number(this.amountNano) === 0); + + if(isRequestingAnyAmount === true) { + this.resetAmount(); + } + + this.merchantModeRawRequestedTotal = + (isRequestingAnyAmount === true) + ? new BigNumber(0) + : this.util.nano.mnanoToRaw(this.amountNano); + + this.merchantModeRawRequestedQR = + (isRequestingAnyAmount === true) + ? new BigNumber(0) + : this.util.nano.mnanoToRaw(this.amountNano); + + this.merchantModeSeenBlockHashes = + this.pendingBlocksForSelectedAccount.reduce( + (seenHashes, receivableBlock) => { + seenHashes[receivableBlock.hash] = true + return seenHashes + }, + {} + ); + + this.merchantModeTransactionHashes = []; + + this.inMerchantModeQR = true; + } + + merchantModeHideQR() { + this.inMerchantModeQR = false; + } + + onMerchantModeReceiveTransaction(transaction) { + if( this.merchantModeSeenBlockHashes[transaction.hash] != null ) { + return; + } + + this.merchantModeSeenBlockHashes[transaction.hash] = true; + + const receivedAmountWithTinyRaws = new BigNumber(transaction.amount); + + const receivedAmount = + this.getRawAmountWithoutTinyRaws(receivedAmountWithTinyRaws); + + const requestedAmount = + this.getRawAmountWithoutTinyRaws(this.merchantModeRawRequestedQR); + + if( receivedAmount.eq(requestedAmount) ) { + this.merchantModeTransactionHashes.push(transaction.hash); + + this.merchantModeMarkCompleteWithAmount(this.merchantModeRawRequestedTotal); + } else { + const transactionPrompt = { + moreThanRequested: receivedAmount.gt(requestedAmount), + lessThanRequested: receivedAmount.lt(requestedAmount), + amountRaw: receivedAmountWithTinyRaws, + amountHiddenRaw: receivedAmountWithTinyRaws.mod(this.nano), + transactionHash: transaction.hash, + } + + this.merchantModePrompts.push(transactionPrompt); + } + } + + merchantModeSubtractAmountFromPrompt(prompt, promptIdx) { + const subtractedRawWithTinyRaws = prompt.amountRaw; + + const subtractedRaw = + this.getRawAmountWithoutTinyRaws(subtractedRawWithTinyRaws); + + const newAmountRaw = + this.merchantModeRawRequestedQR.minus(subtractedRaw); + + this.merchantModeRawRequestedQR = newAmountRaw; + this.changeQRAmount(newAmountRaw.toFixed()); + + this.merchantModeTransactionHashes.push(prompt.transactionHash); + + this.merchantModePrompts.splice(promptIdx, 1); + } + + merchantModeMarkCompleteFromPrompt(prompt) { + this.merchantModeTransactionHashes.push(prompt.transactionHash); + + this.merchantModeMarkCompleteWithAmount(prompt.amountRaw); + } + + merchantModeDiscardPrompt(promptIdx) { + this.merchantModePrompts.splice(promptIdx, 1); + } + + merchantModeMarkCompleteWithAmount(amountRaw) { + this.merchantModeRawReceivedTotal = amountRaw; + this.merchantModeRawReceivedTotalHiddenRaw = amountRaw.mod(this.nano); + + this.inMerchantModePaymentComplete = true; + this.inMerchantModeQR = false; + } + } diff --git a/src/less/components/dark-mode.less b/src/less/components/dark-mode.less index 7988be2..c0e31f5 100644 --- a/src/less/components/dark-mode.less +++ b/src/less/components/dark-mode.less @@ -453,6 +453,16 @@ background: @dark-mode-background-2 !important; } + .merchant-mode-overlay { + background: @dark-mode-background-1 !important; + + .merchant-mode-logo { + background: url('../../assets/img/nault-logo-night-mode.svg'); + background-size: contain; + background-repeat: no-repeat; + } + } + // Representatives page .delegating-account-row, .representative-row { background: @dark-mode-background-3 !important; diff --git a/src/less/nault-theme.less b/src/less/nault-theme.less index 3a1b49d..646eefe 100644 --- a/src/less/nault-theme.less +++ b/src/less/nault-theme.less @@ -28,7 +28,7 @@ h1, h2, h3, h4, h5, .uk-text-lead, .uk-button, .uk-alert, .uk-description-list d @media (max-width: 939px) { .nlt-button-group button { - margin-bottom: @nlt-intro-margin / 2 !important; + margin-bottom: 20px !important; } h2:not(.uk-card-title):not(.uk-modal-title) { @@ -1336,6 +1336,30 @@ input[type=number] { } } +// Receive page +.merchant-centered-container { + .identicon-name-row { + display: flex; + flex-direction: row; + align-items: center; + height: 28px; + padding-bottom: 8px; + + .nano-identicon { + display: inline-block; + width: 27px; + height: 27px; + margin-right: 12px; + margin-left: 1px; + flex-shrink: 0; + + .canvas-container canvas { + border-radius: 3px; + } + } + } +} + // Representatives page .delegating-account-row { border-top: none !important; @@ -1527,6 +1551,14 @@ input[type=number] { background-color: #16A670 !important; } +.nlt-button-green { + background-color: #16A670 !important; + + &:hover { + background-color: #069660 !important; + } +} + .nlt-page-intro { margin-bottom: @nlt-intro-margin; }