diff --git a/.env.example b/.env.example index 968d24ce834..609337e386c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ -AWS_URL_CONFIG = https://domainname/bws/api +// AWS_URL_CONFIG = https://aws-dev.abcpay.cash/bws/api // ENABLE_ANIMATIONS change to false if env is desktop +AWS_URL_CONFIG = http://localhost:3232/bws/api ENABLE_ANIMATIONS = true ACTIVATE_SCANNER = true -LIXI_LOTUS_URL = https://domainname/api \ No newline at end of file +LIXI_LOTUS_URL = https://dev.lixilotus.com/api +BUILD_SWAP_STANDALONE = false \ No newline at end of file diff --git a/package.json b/package.json index 3fdd6f7262b..d2a2e7bb3bf 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "apexcharts": "^3.28.1", "apple-wallet-ng": "^1.1.1", "base64-js": "^1.5.1", + "bignumber.js": "^9.1.0", "bitauth": "git+https://github.com/bitpay/bitauth.git#68cf0353bf517a7e5293478608839fa904351eb6", "buffer-compare": "^1.1.1", "capacitor-resources": "^2.0.5", @@ -138,6 +139,7 @@ "ngx-build-plus": "^13.0.1", "ngx-countdown": "^13.0.0", "ngx-markdown": "^12.0.1", + "ngx-mask": "^13.1.15", "ngx-qrcode2": "^9.0.0", "ngx-text-overflow-clamp": "0.0.1", "nm": "^1.0.0", @@ -268,4 +270,4 @@ "trailingComma": "none", "arrowParens": "avoid" } -} \ No newline at end of file +} diff --git a/scripts/setenv.ts b/scripts/setenv.ts index 6c25ac3cb9d..d54205eec27 100644 --- a/scripts/setenv.ts +++ b/scripts/setenv.ts @@ -30,6 +30,7 @@ let awsUrl = ? awsUrlCLI : process.env.AWS_URL_CONFIG; let lixiLotusUrl = process.env.LIXI_LOTUS_URL; +let buildSwapAlone = process.env.BUILD_SWAP_STANDALONE; if (environment === 'production') { nameEnv = 'production'; } else if (environment === 'development') { @@ -49,7 +50,8 @@ export const env = { ratesAPI: new CurrencyProvider().getRatesApi(), activateScanner: ${activateScanner}, awsUrl: '${awsUrl}', - lixiLotusUrl: '${lixiLotusUrl}' + lixiLotusUrl: '${lixiLotusUrl}' , + buildSwapALone: ${buildSwapAlone} }; export default env;`; diff --git a/src/app/app-init.service.ts b/src/app/app-init.service.ts new file mode 100644 index 00000000000..6834574e808 --- /dev/null +++ b/src/app/app-init.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +@Injectable() +export class AppInitService { + + constructor(private Router: Router) {} + + init() { + return new Promise((resolve, reject) => { + + // Simple example from an array. In reality, I used the response of + // a GET. Important thing is that the app will wait for this promise to resolve + // const newDynamicRoutes = [{ + // routeName: '', + // component: 'SwapPage' + // }] + const routes = this.Router.config; + // routes.push({ path: '', component: SwapPage }); + this.Router.resetConfig(routes); + resolve(); + }); + } +} \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3c59eba6922..818aa3d053b 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -67,273 +67,587 @@ import { SettingsPage } from './pages/settings/settings'; import { SearchContactPage } from './pages/search/search-contact/search-contact.component'; import { SelectFlowPage } from './pages/onboarding/select-flow/select-flow'; import { ChartViewPage } from './pages/chart-view/chart-view'; +import { FeatureGuard } from './providers/feature-gaurd.service'; +import { CreateSwapPage } from './pages/swap/create-swap/create-swap.component'; +import { OrderSwapPage } from './pages/swap/order-swap/order-swap.component'; const routes: Routes = [ { path: '', - loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule) + loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule), + canLoad: [FeatureGuard], + data: { + feature: 'abcpay' + } }, { path: 'select-flow', - component: SelectFlowPage + component: SelectFlowPage, + canActivate: [FeatureGuard], + data: { + feature: 'abcpay' + } }, { path: 'feature-education', - component: FeatureEducationPage + component: FeatureEducationPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'about', component: AboutPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'accounts-page', - component: AccountsPage + component: AccountsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'token-details', - component: TokenDetailsPage + component: TokenDetailsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'token-info', - component: TokenInforPage + component: TokenInforPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'confirm-token', - component: ConfirmTokenPage + component: ConfirmTokenPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'alt-curency', - component: AltCurrencyPage, + component: AltCurrencyPage }, { path: 'language', - component: LanguagePage, + component: LanguagePage }, { path: 'advanced', component: AdvancedPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'local-theme', - component: LocalThemePage, + component: LocalThemePage }, { path: 'navigation', component: NavigationPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'addressbook', component: AddressbookPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'fee-policy', component: FeePolicyPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'notifications', component: NotificationsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-settings', component: WalletSettingsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'share', component: SharePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'lock', component: LockPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'key-settings', component: KeySettingsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'add', component: AddPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-recover', component: WalletRecoverPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'address-book-add', component: AddressbookAddPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'address-book-view', component: AddressbookViewPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'scan', component: ScanPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'price', component: PricePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'session-log', component: SessionLogPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'send-feedback', component: SendFeedbackPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'share', component: SharePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'select-currency', component: SelectCurrencyPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'join-wallet', component: JoinWalletPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'create-wallet', component: CreateWalletPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'import-wallet', component: ImportWalletPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'recovery-key', component: RecoveryKeyPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-details', component: WalletDetailsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'send-page', component: SendPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'add-wallet', component: AddWalletPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'amount', component: AmountPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-name', component: WalletNamePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-information', component: WalletInformationPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-addresses', component: WalletAddressesPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-export', component: WalletExportPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-service-url', component: WalletServiceUrlPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-transaction-history', component: WalletTransactionHistoryPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-duplicate', component: WalletDuplicatePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-delete', component: WalletDeletePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'wallet-mnemonic-recover', component: WalletMnemonicRecoverPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'backup-key', component: BackupKeyPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'backup-game', component: BackupGamePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'clear-encrypt-password', component: ClearEncryptPasswordPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'key-delete', component: KeyDeletePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'key-qr-export', component: KeyQrExportPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'extended-private-key', component: ExtendedPrivateKeyPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'key-name', component: KeyNamePage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'lock-method', component: LockMethodPage, - canActivate: [RedirectGuard] + canActivate: [RedirectGuard], + data: { + feature: 'abcpay' + } }, { path: 'proposals-notifications', component: ProposalsNotificationsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'confirm', component: ConfirmPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'multi-send', component: MultiSendPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'select-inputs', component: SelectInputsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'send-select-inputs', component: SelectInputsSendPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'transfer-to-modal', component: TransferToModalPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'custom-amount', component: CustomAmountPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'add-funds', component: AddFundsPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'copayers', component: CopayersPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'paper-wallet', component: PaperWalletPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'address-book-add', component: AddressbookAddPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'setting', @@ -341,11 +655,39 @@ const routes: Routes = [ }, { path: 'search-contact', - component: SearchContactPage + component: SearchContactPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } }, { path: 'chart-view', - component: ChartViewPage + component: ChartViewPage, + canActivate: [FeatureGuard], + + data: { + feature: 'abcpay' + } + }, + { + path: 'create-swap', + component: CreateSwapPage, + canActivate: [FeatureGuard], + + data: { + feature: 'swap' + } + }, + { + path: 'order-swap', + component: OrderSwapPage, + canActivate: [FeatureGuard], + + data: { + feature: 'swap' + } } ]; diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 0c66fb0a6c2..49cd2bec12e 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -132,7 +132,7 @@ body { wide-header-page { ion-content { - --padding-bottom: 7rem !important; + --padding-bottom: 7rem; } } @@ -2284,4 +2284,89 @@ ion-popover { } } } +} + +.mat-option { + height: fit-content !important; + background: #FAFAFB; + border: 1px solid rgba(0, 30, 46, 0.38); + border-top: 0; + border-left: 0; + border-right: 0; + .mat-option-text { + display: flex; + align-items: center; + height: fit-content; + padding: 1rem 0; + img { + width: 40px; + height: 40px; + margin-right: 8px; + } + .mat-coin-name { + margin: 0; + line-height: initial; + color: #001E2E; + font-weight: 600; + font-size: 16px; + } + .mat-symbol-name { + margin: 0; + line-height: initial; + color: rgba(0, 30, 46, 0.6); + letter-spacing: 0.4px; + font-size: 12px; + margin-top: 5px; + } + } + &:first-child { + .mat-option-text { + padding: 0; + font-size: 14px; + line-height: 32px; + font-weight: 500; + } + } + &.dark { + background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E !important; + border-color: #647a86; + .mat-option-text { + color: #EDEFF0 !important; + .mat-coin-name { + color: #EDEFF0 !important; + } + .mat-symbol-name { + color: rgba(237, 239, 240, 0.6) !important; + } + } + } +} + +mat-select-trigger { + display: flex !important; + img { + width: 40px; + height: 40px; + margin-right: 8px; + } + .mat-coin-name { + margin: 0; + line-height: initial; + color: #001E2E; + font-weight: 600; + font-size: 16px; + } + .mat-symbol-name { + margin: 0; + line-height: initial; + color: rgba(0, 30, 46, 0.6); + letter-spacing: 0.4px; + font-size: 12px; + margin-top: 5px; + } +} + +.mat-select-panel { + border: 1px solid #d5e9f4; + border-radius: 12px; } \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 297d99a7265..7a0961c6035 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -30,6 +30,7 @@ import { PinModalPage } from './pages/pin/pin-modal/pin-modal'; import { FingerprintModalPage } from './pages/fingerprint/fingerprint'; import { ImageLoaderConfigService } from 'ionic-image-loader-v5'; import { CopayersPage } from './pages/add/copayers/copayers'; +import { FeatureFlagsService } from './providers/feature-flags.service'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -60,6 +61,7 @@ export class CopayApp { private onResumeSubscription: Subscription; private isCopayerModalOpen: boolean; private copayerModal: any; + private isSwap: boolean = false; constructor( private config: Config, private platform: Platform, @@ -89,16 +91,21 @@ export class CopayApp { private addressBookProvider: AddressBookProvider, private router: Router, private imageLoaderConfig: ImageLoaderConfigService, - private navasd: NavController + private navasd: NavController, + private featureFlagService: FeatureFlagsService ) { + this.isSwap = this.featureFlagService.isFeatureEnabled('swap'); + if(this.featureFlagService.isFeatureEnabled('abcpay')){ + this.isSwap = false; + } this.imageLoaderConfig.setFileNameCachedWithExtension(true); this.imageLoaderConfig.useImageTag(true); this.imageLoaderConfig.enableSpinner(false); - this.initializeApp(); this.platformProvider.isCordova ? this.routerHidden = true : this.routerHidden = false; if (!this.platformProvider.isCordova) { this.renderer.addClass(document.body, 'bg-desktop'); } + this.initializeApp(); } ngOnDestroy() { @@ -250,12 +257,9 @@ export class CopayApp { this.themeProvider.apply(); if (this.platformProvider.isElectron) this.updateDesktopOnFocus(); - this.incomingDataRedirEvent(); - this.events.subscribe('OpenWallet', (wallet, params) => - this.openWallet(wallet, params) - ); - let profile; + if(!this.isSwap){ + let profile; this.keyProvider .load() .then(() => { @@ -309,6 +313,11 @@ export class CopayApp { this.logsProvider.get(this.appProvider.info.nameCase, platform); }); }); + } + this.events.subscribe('OpenWallet', (wallet, params) => + this.openWallet(wallet, params) + ); + await this.persistenceProvider.setTempMdesCertOnlyFlag('disabled'); this.addressBookProvider.migrateOldContacts(); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a384472666f..02aa17f3a46 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,5 +1,5 @@ import { HttpClientModule } from '@angular/common/http'; -import { CUSTOM_ELEMENTS_SCHEMA, ErrorHandler, NgModule } from '@angular/core'; +import { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, ErrorHandler, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouteReuseStrategy } from '@angular/router'; import { ServiceWorkerModule } from '@angular/service-worker'; @@ -42,6 +42,7 @@ import { enterAnimation } from './animations/nav-animation'; import { MatGridListModule } from '@angular/material/grid-list'; import { MatFormFieldModule } from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatInputModule } from '@angular/material/input'; import { MatIconModule } from '@angular/material/icon'; @@ -50,6 +51,14 @@ import { ClickOutsideModule } from 'ng-click-outside'; import { CountdownModule } from 'ngx-countdown'; import { RECAPTCHA_V3_SITE_KEY, RecaptchaV3Module } from "ng-recaptcha"; +import { NgxMaskModule } from 'ngx-mask'; +import { FeatureFlagsService } from './providers/feature-flags.service'; + +const featureFactory = (featureFlagsService: FeatureFlagsService) => () => + featureFlagsService.loadConfig(); + + + export function translateParserFactory() { return new InterpolatedTranslateParser(); } @@ -95,8 +104,10 @@ export class MyMissingTranslationHandler implements MissingTranslationHandler { backButtonText: '', navAnimation: enterAnimation }), + NgxMaskModule.forRoot(), MatGridListModule, MatFormFieldModule, + MatSelectModule, BrowserAnimationsModule, MatIconModule, MatInputModule, @@ -133,6 +144,13 @@ export class MyMissingTranslationHandler implements MissingTranslationHandler { ServiceWorkerModule.register('ngsw-worker.js', { enabled: env.name === 'production' }) ], providers: [ + { + provide: APP_INITIALIZER, + useFactory: featureFactory, + deps: [FeatureFlagsService], + multi: true + }, + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy diff --git a/src/app/components/wallet-detail-card/wallet-detail-card.component.ts b/src/app/components/wallet-detail-card/wallet-detail-card.component.ts index fb29489109b..d2f8f0e4daf 100644 --- a/src/app/components/wallet-detail-card/wallet-detail-card.component.ts +++ b/src/app/components/wallet-detail-card/wallet-detail-card.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, Input, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, Output, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; import { Router } from '@angular/router'; import { ActionSheetProvider, AddressProvider, AppProvider, BwcErrorProvider, ConfigProvider, CurrencyProvider, ErrorsProvider, EventManagerService, Logger, PlatformProvider, ProfileProvider, RateProvider, TokenProvider, WalletProvider } from 'src/app/providers'; import { DecimalFormatBalance } from 'src/app/providers/decimal-format.ts/decimal-format'; @@ -37,6 +37,8 @@ export class WalletDetailCardComponent { @Input() flagAllItemRemove: boolean = false; + @Output() genNewAddressEvent = new EventEmitter(); + @ViewChild('slidingItem') slidingItem: IonItemSliding; @ViewChild('itemWallet') itemWallet: ElementRef; @@ -274,7 +276,11 @@ export class WalletDetailCardComponent { await timer(400).toPromise(); } this.address = address; - + if(this.wallet && this.wallet.etokenAddress){ + const { prefix, type, hash } = this.addressProvider.decodeAddress(address); + this.wallet.etokenAddress = this.addressProvider.encodeAddress('etoken', type, hash, address); + } + this.genNewAddressEvent.emit(true); await timer(200).toPromise(); this.playAnimation = false; } diff --git a/src/app/directives/feature-flags/feature-flags.ts b/src/app/directives/feature-flags/feature-flags.ts new file mode 100644 index 00000000000..0a90670de3e --- /dev/null +++ b/src/app/directives/feature-flags/feature-flags.ts @@ -0,0 +1,27 @@ +import { + Directive, + Input, + OnInit, + TemplateRef, + ViewContainerRef + } from "@angular/core"; +import { FeatureFlagsService } from "src/app/providers/feature-flags.service"; + + @Directive({ + selector: "[featureFlag]" + }) + export class FeatureFlagDirective implements OnInit { + @Input() featureFlag: string; + constructor( + private tpl: TemplateRef, + private vcr: ViewContainerRef, + private featureFlagService: FeatureFlagsService + ) {} + + ngOnInit() { + const isEnabled = this.featureFlagService.isFeatureEnabled(this.featureFlag); + if (isEnabled) { + this.vcr.createEmbeddedView(this.tpl); + } + } + } \ No newline at end of file diff --git a/src/app/pages/add/create-wallet/create-wallet.html b/src/app/pages/add/create-wallet/create-wallet.html index 91eb27705e4..b4165da23fc 100755 --- a/src/app/pages/add/create-wallet/create-wallet.html +++ b/src/app/pages/add/create-wallet/create-wallet.html @@ -177,7 +177,7 @@ [ngClass]="{'with-label': createForm.value.singleAddress}">

{{ 'Single Address' | translate }}

- +
The single address feature will force the account to only use one address rather than diff --git a/src/app/pages/home/home.html b/src/app/pages/home/home.html index 6c9ddb45e0e..f2d5de9edc0 100644 --- a/src/app/pages/home/home.html +++ b/src/app/pages/home/home.html @@ -18,6 +18,9 @@

{{ 'Home' | translate }}

+ + + diff --git a/src/app/pages/home/home.ts b/src/app/pages/home/home.ts index 329cdc619da..bfd42848271 100644 --- a/src/app/pages/home/home.ts +++ b/src/app/pages/home/home.ts @@ -566,6 +566,10 @@ export class HomePage { this.router.navigate(['/chart-view']); } + public openCreateSwapPage(): void { + this.router.navigate(['/create-swap']); + } + public addToHome(coin?: string, network?: string) { this.router.navigateByUrl('/accounts-page', { state: { diff --git a/src/app/pages/pages.ts b/src/app/pages/pages.ts index 004c99eb2b0..6014368bc3c 100644 --- a/src/app/pages/pages.ts +++ b/src/app/pages/pages.ts @@ -111,6 +111,11 @@ import { AccountsPage } from './accounts/accounts'; import { SearchContactPage } from './search/search-contact/search-contact.component'; import { SelectFlowPage } from './onboarding/select-flow/select-flow'; +/* Swap */ +import { CreateSwapPage } from './swap/create-swap/create-swap.component'; +import { OrderSwapPage } from './swap/order-swap/order-swap.component'; + + export const PAGES = [ AddPage, AddWalletPage, @@ -152,6 +157,8 @@ export const PAGES = [ FingerprintModalPage, HomePage, ChartViewPage, + CreateSwapPage, + OrderSwapPage, // CardsPage, WalletsPage, AccountsPage, @@ -166,7 +173,7 @@ export const PAGES = [ ...PIN_COMPONENTS, PricePage, ProposalsNotificationsPage, - ScanPage, + ScanPage, SendPage, SettingsPage, SelectCurrencyPage, @@ -206,5 +213,7 @@ export const PAGES = [ LocalThemePage, NavigationPage, NewFeaturePage, + CreateSwapPage, + OrderSwapPage // Phases: card pages ]; diff --git a/src/app/pages/settings/settings.html b/src/app/pages/settings/settings.html index 4750876b764..26588198d34 100644 --- a/src/app/pages/settings/settings.html +++ b/src/app/pages/settings/settings.html @@ -7,7 +7,7 @@ - + {{ 'Settings' | translate }} @@ -40,7 +40,6 @@ {{appVersion}} - {{ 'Theme' | translate }} @@ -76,99 +75,102 @@ {{ currentLanguageName | translate }} - - - - {{ 'Lock App' | translate }} - - - {{ 'Disabled' | translate }} - - - {{ 'PIN' | translate }} - - - {{ 'Biometric' | translate }} - - - - - {{ 'Show Portfolio Value' | translate }} - - - - - - {{ 'Your Keys' | translate }} - - - - - - {{profileProvider.getWalletGroup(walletsGroup[0].keyId).name}} - - - {{'{walletsGroupLength} Account' | translate:{walletsGroupLength: walletsGroup.length} }} - - - {{'{walletsGroupLength} Accounts' | translate:{walletsGroupLength: walletsGroup.length} }} - - - - - - {{'Read Only' | translate}} - - - {{'{walletsGroupLength} Account' | translate:{walletsGroupLength: readOnlyWalletsGroup.length} }} - - - {{'{walletsGroupLength} Accounts' | translate:{walletsGroupLength: readOnlyWalletsGroup.length} }} - - - - - - {{ 'Create or Import a Key' | translate }} - - - - - - {{ 'Other' | translate }} - - - - - - {{'Address Book' | translate}} - - - - - - {{'Network Fee Policies' | translate}} - - - - - - {{'Advanced' | translate}} - - - - - - Help & Support - - - - - - {{'About {appName}' | translate: {appName: appName} }} - - + + + + {{ 'Lock App' | translate }} + + + {{ 'Disabled' | translate }} + + + {{ 'PIN' | translate }} + + + {{ 'Biometric' | translate }} + + + + + {{ 'Show Portfolio Value' | translate }} + + + + + + + {{ 'Your Keys' | translate }} + + + + + + {{profileProvider.getWalletGroup(walletsGroup[0].keyId).name}} + + + {{'{walletsGroupLength} Account' | translate:{walletsGroupLength: walletsGroup.length} }} + + + {{'{walletsGroupLength} Accounts' | translate:{walletsGroupLength: walletsGroup.length} }} + + + + + + {{'Read Only' | translate}} + + + {{'{walletsGroupLength} Account' | translate:{walletsGroupLength: readOnlyWalletsGroup.length} }} + + + {{'{walletsGroupLength} Accounts' | translate:{walletsGroupLength: readOnlyWalletsGroup.length} }} + + + + + + {{ 'Create or Import a Key' | translate }} + + + + + + {{ 'Other' | translate }} + + + + + + {{'Address Book' | translate}} + + + + + + {{'Network Fee Policies' | translate}} + + + + + + {{'Advanced' | translate}} + + + + + + Help & Support + + + + + + {{'About {appName}' | translate: {appName: appName} }} + + + +
diff --git a/src/app/pages/settings/settings.ts b/src/app/pages/settings/settings.ts index e1b4cd2dcee..e309845c502 100644 --- a/src/app/pages/settings/settings.ts +++ b/src/app/pages/settings/settings.ts @@ -27,7 +27,7 @@ import { EventManagerService } from 'src/app/providers/event-manager.service'; import { Router } from '@angular/router'; import { NewFeaturePage } from '../new-feature/new-feature'; import { ActionSheetProvider } from 'src/app/providers'; - +import { FeatureFlagsService } from '../../providers/feature-flags.service'; @Component({ selector: 'page-settings', templateUrl: 'settings.html', @@ -76,6 +76,7 @@ export class SettingsPage { public navigation: string; public featureList: any; public isScroll = false; + public isAbcpay = false; useLegacyQrCode; constructor( private app: AppProvider, @@ -95,12 +96,14 @@ export class SettingsPage { private themeProvider: ThemeProvider, private events: EventManagerService, private newFeatureData: NewFeatureData, - private router: Router + private router: Router, + private featureFlagsService: FeatureFlagsService ) { this.appName = this.app.info.nameCase; this.appVersion = this.app.info.version; this.isCordova = this.platformProvider.isCordova; this.isCopay = this.app.info.name === 'copay'; + this.isAbcpay = this.featureFlagsService.isFeatureEnabled('abcpay'); } async handleScrolling(event) { diff --git a/src/app/pages/swap/config-swap.ts b/src/app/pages/swap/config-swap.ts new file mode 100644 index 00000000000..a898257acae --- /dev/null +++ b/src/app/pages/swap/config-swap.ts @@ -0,0 +1,90 @@ +export interface TokenInfo { + coin: string; + blockCreated?: number; + circulatingSupply?: number; + containsBaton: true; + decimals: number; + documentHash?: string; + documentUri: string; + id: string; + initialTokenQty: number; + name: string; + symbol: string; + timestamp: string; + timestamp_unix?: number; + totalBurned: number; + totalMinted: number; + versionType: number; +} + +export interface TokenItem{ + tokenId : string; + tokenInfo: TokenInfo, + amountToken: number, + utxoToken: any; +} + + +export class ConfigSwap { + coinSwap: CoinConfig[]; + coinReceive: CoinConfig[]; + static create(opts){ + const x = new ConfigSwap(); + x.coinReceive = opts.coinReceive; + x.coinSwap = opts.coinSwap; + return x; + } + static fromObj(opts){ + const x = new ConfigSwap(); + x.coinReceive = opts.coinReceive; + x.coinSwap = opts.coinSwap; + + return x; + } +} + +export class CoinConfig{ + code: string; + isToken: boolean; + networkFee?: number; + rate?: any; + min?: number; + minConvertToSat?: number; + max?: number; + maxConvertToSat?: number; + tokenInfo?: TokenInfo; + isEnable?: boolean; + network: string; + + static create(opts){ + const x = new CoinConfig(); + x.code = opts.code; + x.isToken = opts.isToken; + x.networkFee = opts.networkFee || 0; + x.rate = null; + x.min = opts.min || 0; + x.minConvertToSat = opts.minConvertToSat || 0; + x.max = opts.max || 0; + x.maxConvertToSat = opts.maxConvertToSat || 0; + x.tokenInfo = opts.tokenInfo || null; + x.isEnable = opts.isEnable || true; + x.network = opts.network; + return x; + } + + static fromObj(opts){ + const x = new CoinConfig(); + x.code = opts.code; + x.isToken = opts.isToken; + x.networkFee = opts.networkFee; + x.rate = opts.rate; + x.min = opts.min; + x.minConvertToSat = opts.minConvertToSat; + x.max = opts.max; + x.maxConvertToSat = opts.maxConvertToSat; + x.tokenInfo = opts.tokenInfo; + x.isEnable = opts.isEnable; + x.network = opts.network; + return x; + } +} diff --git a/src/app/pages/swap/create-swap/create-swap.component.html b/src/app/pages/swap/create-swap/create-swap.component.html new file mode 100644 index 00000000000..6825a009586 --- /dev/null +++ b/src/app/pages/swap/create-swap/create-swap.component.html @@ -0,0 +1,171 @@ + + + + +
+ {{ 'Swap'| translate }} +
+
+ + + + +
+
+ + +
+ + +
+ +
+
+ {{ 'Swap' | translate }} +
+ + + + +
+
+ + 1  + {{ coinSwapSelected.code.toUpperCase() }} + + {{ + formatAmountWithLimitDecimal( + coinSwapSelected.rate.USD / coinReceiveSelected.rate.USD, + 8 + ) + }}  {{ coinReceiveSelected.code.toUpperCase() }} +
+ + +
+
+
+
+
+
+
+
You send
+
+ + + + +
+

{{coinSwapSelected.code.toUpperCase()}}

+

{{getCoinName(coinSwapSelected)}}

+
+
+ + Select coin send + + + +
+

{{coin.code.toUpperCase()}}

+

{{getCoinName(coin)}}

+
+
+
+
+ + + + +

+ {{ altValue.toFixed() | mask: 'separator.8':',' }} + {{ fiatCode }} +

+
+
+
+
You receive
+
+ + + + +
+

{{coinReceiveSelected.code.toUpperCase()}}

+

{{getCoinName(coinReceiveSelected)}}

+
+
+ + Select coin receive + + + +
+

{{coin.code.toUpperCase()}}

+

{{getCoinName(coin)}}

+
+
+
+
+ + + + +

+ {{ altValue.toFixed() | mask: 'separator.8':',' }} + {{ fiatCode }} +

+
+
+
+ You need to deposit at + least + {{ minWithCurrentFiat | mask: 'separator.8':',' }} {{ fiatCode }} + + You can not deposit + above + {{ maxWithCurrentFiat | mask: 'separator.8':',' }} {{ fiatCode }} + +
+ Network fee: + + {{ + coinReceiveSelected.networkFee + | satToUnit: getChain(coinReceiveSelected.code) + }} + + + {{ getFeeToken(coinReceiveSelected.networkFee) }} XEC + +
+ +
+

Your {{ getCoinName(coinReceiveSelected) }} address

+ + + + + Wrong format addresss + +
+
+
+
+ + + +
+ + {{ 'Swap' | translate }} +
+
+
+
\ No newline at end of file diff --git a/src/app/pages/swap/create-swap/create-swap.component.scss b/src/app/pages/swap/create-swap/create-swap.component.scss new file mode 100644 index 00000000000..ce1f07e1eec --- /dev/null +++ b/src/app/pages/swap/create-swap/create-swap.component.scss @@ -0,0 +1,170 @@ +page-create-swap { + .header-container { + display: flex; + flex-flow: row; + justify-content: space-between; + align-items: center; + .header-title { + font-weight: 400; + font-size: 32px; + color: #001e2e; + } + .search-container { + --background: linear-gradient( + 0deg, + rgba(0, 93, 141, 0.05), + rgba(0, 93, 141, 0.05) + ), + #fafafb; + background: linear-gradient( + 0deg, + rgba(0, 93, 141, 0.05), + rgba(0, 93, 141, 0.05) + ), + #fafafb; + --padding-start: 8px; + --padding-top: 0; + --padding-end: 0; + --padding-bottom: 0; + --inner-padding-end: 0; + border: 1px solid rgba(28, 55, 69, 0.6); + border-radius: 4px; + padding: 0; + max-height: 40px; + display: flex; + .search-tx-modal { + --placeholder-color: #001e2e; + --color: #001e2e; + font-size: 14px; + letter-spacing: 0.5px; + } + + ion-icon { + margin: 0; + margin-right: 8px; + width: 24px; + height: 24px; + color: rgba(28, 55, 69, 0.6); + } + } + } + .swap-exchange-rate-container { + font-size: 14px; + font-weight: 400; + display: flex; + align-items: center; + margin: 2rem 0; + .swap-ico { + margin: 0 8px; + } + .count-down-container { + margin-left: 8px; + display: flex; + align-items: center; + .count-down { + font-weight: 600; + font-size: 12px; + line-height: 16px; + text-align: center; + letter-spacing: 0.5px; + color: rgba(0, 30, 46, 0.6); + margin-left: 8px; + } + img { + position: absolute; + } + } + } + .swap-exchange-container { + width: 100%; + background: #fafafb; + border-radius: 12px; + display: flex; + flex-direction: row; + align-items: flex-start; + border: 1px #d5e9f4 solid; + .hint { + margin: 0; + font-size: 12px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.38); + } + ion-item { + margin: 1rem 0; + --background: #fafafb; + --padding-start: 0; + } + mat-form-field { + max-height: 80px; + padding: 0; + .mat-form-field-infix { + padding: 10px 0 1rem 0 !important; + } + } + .title { + font-weight: 600; + font-size: 14px; + letter-spacing: 0.1px; + color: #001e2e; + } + } + .send-container { + width: 50%; + order: 0; + flex-grow: 1; + padding: 1rem; + } + .receive-container { + width: 50%; + order: 2; + flex-grow: 1; + border-left: 1px rgba(112, 129, 138, 0.38) solid; + padding: 1rem; + } + .network-fee-line { + text-align: right; + margin: 2rem 0; + .label-network { + color: rgba(28, 55, 69, 0.6); + font-size: 13px; + } + .amount-fee-network { + font-size: 13px; + } + } + .address-container { + padding: 1rem 1rem 2rem 1rem; + background: #fafafb; + border-radius: 12px; + border: 1px #d5e9f4 solid; + p { + color: #001e2e; + font-weight: 600; + letter-spacing: 0.1px; + margin: 0 0 10px 0; + } + mat-form-field { + padding: 0; + .mat-form-field-infix { + padding: 10px 0 1em 0; + font-size: 14px; + } + } + } + mat-error { + font-size: 12px; + padding-left: 1rem; + } + + @media screen and (max-width: 390px) { + .header-container { + padding: 0 1rem; + } + .swap-exchange-rate-container { + padding: 0 1rem; + } + .swap-form { + padding: 0 1rem; + } + } +} diff --git a/src/app/pages/swap/create-swap/create-swap.component.spec.ts b/src/app/pages/swap/create-swap/create-swap.component.spec.ts new file mode 100644 index 00000000000..af398d837f3 --- /dev/null +++ b/src/app/pages/swap/create-swap/create-swap.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { CreateSwapComponent } from './create-swap.component'; + +describe('CreateSwapComponent', () => { + let component: CreateSwapComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ CreateSwapComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(CreateSwapComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/swap/create-swap/create-swap.component.ts b/src/app/pages/swap/create-swap/create-swap.component.ts new file mode 100644 index 00000000000..68ddd1dde70 --- /dev/null +++ b/src/app/pages/swap/create-swap/create-swap.component.ts @@ -0,0 +1,615 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AbstractControl, + FormBuilder, + FormGroup, + ValidationErrors, + ValidatorFn +} from '@angular/forms'; +import { Router } from '@angular/router'; +import _ from 'lodash'; +import { CountdownComponent } from 'ngx-countdown'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; +import { + AddressProvider, + BwcErrorProvider, + Coin, + CurrencyProvider, + ErrorsProvider, + IncomingDataProvider, + RateProvider, + ThemeProvider +} from 'src/app/providers'; +import { OrderProvider } from 'src/app/providers/order/order-provider'; +import { Config, ConfigProvider } from '../../../providers/config/config'; +import { CoinConfig, ConfigSwap } from '../config-swap'; +import BigNumber from "bignumber.js"; +import { TranslateService } from '@ngx-translate/core'; + +interface TokenInfo { + coin: string; + blockCreated?: number; + circulatingSupply?: number; + containsBaton: true; + decimals: number; + documentHash?: string; + documentUri: string; + id: string; + initialTokenQty: number; + name: string; + symbol: string; + timestamp: string; + timestamp_unix?: number; + totalBurned: number; + totalMinted: number; + versionType: number; +} + +interface OrderOpts { + fromCoinCode: string; + amountFrom: number; + isFromToken: boolean; + fromTokenId?: string; + toCoinCode: string; + isToToken: boolean; + toTokenId?: string; + createdRate: number; + addressUserReceive: string; + fromSatUnit?: number; + toSatUnit?: number; + toTokenInfo? : TokenInfo; + fromTokenInfo?: TokenInfo; + fromNetwork: string; + toNetwork: string; +} +interface IOrder { + id: string | number; + version: number; + priority: number; + fromCoinCode: string; + fromTokenId?: string; + amountFrom: number; + fromSatUnit?: number; + isFromToken?: boolean; + toCoinCode: string; + isToToken: boolean; + toSatUnit: number; + amountSentToUser: number; + amountUserDeposit: number; + createdRate: number; + updatedRate: number; + addressUserReceive: string; + adddressUserDeposit: string; + toTokenId?: string; + listTxIdUserDeposit?: string[]; + listTxIdUserReceive?: string[]; + status?: string; + isSentToFund?: boolean; + isSentToUser?: boolean; + endedOn?: number; + createdOn?: number; + error?: string; + toTokenInfo? : TokenInfo; + fromTokenInfo?: TokenInfo; + fromNetwork: string; + toNetwork: string; +} + +@Component({ + selector: 'page-create-swap', + templateUrl: './create-swap.component.html', + styleUrls: ['./create-swap.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CreateSwapPage implements OnInit { + public isScroll = false; + public currentTheme: any; + public rates: any; + public coinSwapSelected: CoinConfig; + public coinReceiveSelected: CoinConfig; + public altValue : BigNumber = new BigNumber(0); + private modelChanged: Subject = new Subject(); + private subscription: Subscription; + public usdRate: any; + public fiatCode: any; + public config: Config; + public addressSwapValue: any; + public validAddress: any; + public minAmount: any; + public minWithCurrentFiat: any; + public maxWithCurrentFiat: any; + public createForm: FormGroup; + public searchValue = ''; + debounceTime = 500; + @ViewChild('cd', { static: false }) private countdown: CountdownComponent; + @ViewChild('inputSwap') inputSwap: ElementRef; + @ViewChild('inputReceive') inputReceive: ElementRef; + private validDataTypeMap: string[] = [ + 'BitcoinAddress', + 'BitcoinCashAddress', + 'ECashAddress', + 'LotusAddress', + 'EthereumAddress', + 'EthereumUri', + 'RippleAddress', + 'DogecoinAddress', + 'LitecoinAddress', + 'RippleUri', + 'BitcoinUri', + 'BitcoinCashUri', + 'DogecoinUri', + 'LitecoinUri', + 'BitPayUri', + 'ECashUri', + 'LotusUri' + ]; + + public listConfig: ConfigSwap = null; + + constructor( + private router: Router, + private themeProvider: ThemeProvider, + private rateProvider: RateProvider, + private currencyProvider: CurrencyProvider, + private configProvider: ConfigProvider, + private incomingDataProvider: IncomingDataProvider, + private addressProvider: AddressProvider, + private form: FormBuilder, + private _cdRef: ChangeDetectorRef, + private orderProvider: OrderProvider, + private errorsProvider: ErrorsProvider, + private translate: TranslateService, + private bwcErrorProvider: BwcErrorProvider + ) { + this.createForm = this.form.group({ + swapAmount: [ + 0, + { + validators: [this.amountMinValidator(true), this.amountMaxValidator()], + updateOn: 'change' + } + ], + receiveAmount: [ + 0, + { + validators: [this.amountMinValidator(false), this.amountMaxValidator()], + updateOn: 'change' + } + ], + address: [ + null, + { + validators: [this.addressValidator()], + updateOn: 'change' + } + ] + }); + } + + public getChain(coin: string): string { + return this.currencyProvider.getChain(coin as Coin).toLowerCase(); + } + + public convertAmountToSatoshiAmount(coinConfig, amount): number { + if (coinConfig.isToken) { + const decimals = coinConfig.tokenInfo.decimals; + return amount * Math.pow(10, decimals); + } else { + const precision = _.get( + this.currencyProvider.getPrecision(coinConfig.code), + 'unitToSatoshi', + 0 + ); + return amount * precision; + } + } + + public getSatUnitFromCoin(coinConfig) { + if (coinConfig.isToken) { + const decimals = coinConfig.tokenInfo.decimals; + return Math.pow(10, decimals); + } else { + const precision = _.get( + this.currencyProvider.getPrecision(coinConfig.code), + 'unitToSatoshi', + 0 + ); + return precision; + } + } + + handleEvent(event) { + if (event.action === 'done') { + this.countdown.restart(); + this.handleUpdateRate(); + } + } + + async handleScrolling(event) { + if (event.detail.currentY > 0) { + this.isScroll = true; + } else { + this.isScroll = false; + } + } + + getFeeToken(networkFee): number { + const precision = _.get( + this.currencyProvider.getPrecision('xec' as Coin), + 'unitToSatoshi', + 0 + ); + if (!precision) { + return 0; + } else { + return networkFee / precision; + } + } + + ngOnInit() { + this.orderProvider.getConfigSwap().then(configSwap => { + this.listConfig = configSwap; + this._cdRef.markForCheck(); + this.orderProvider + .getTokenInfo() + .then((listTokenInfo: TokenInfo[]) => { + const allConig = this.listConfig.coinSwap.concat( + this.listConfig.coinReceive + ); + allConig.forEach(coinConfig => { + if (coinConfig.isToken) { + coinConfig.tokenInfo = listTokenInfo.find( + s => s.symbol.toLowerCase() === coinConfig.code + ); + } + }); + }) + .catch(err => { + console.log(err); + }); + this.coinReceiveSelected = this.listConfig.coinReceive[0]; + this.coinSwapSelected = this.listConfig.coinSwap[0]; + this.subscription = this.modelChanged + .pipe(debounceTime(this.debounceTime)) + .subscribe(isSwap => { + this.handleInputChange(isSwap); + }); + }); + } + + getCoinName(coin: CoinConfig) { + const objCoin = this.currencyProvider.getCoin(coin.code.toUpperCase()); + const nameCoin = this.currencyProvider.getCoinName(objCoin) || ''; + return nameCoin; + } + + getImageCoin(coin: CoinConfig) { + return !coin.isToken ? `assets/img/currencies/${coin.code}.svg` : `assets/img/currencies/${coin.tokenInfo.symbol}.svg`; + } + + handleInputChange(isSwap: Boolean) { + if (!!isSwap) { + const result = + this.createForm.controls['swapAmount'].value * + (this.coinSwapSelected.rate.USD / this.coinReceiveSelected.rate.USD); + this.createForm.controls['receiveAmount'].setValue(result); + } else { + const result = + this.createForm.controls['receiveAmount'].value * + (this.coinReceiveSelected.rate.USD / this.coinSwapSelected.rate.USD); + this.createForm.controls['swapAmount'].setValue(result); + } + } + + amountMinValidator(isSwap: boolean): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.value === 0) { + return null; + } + + if (!!isSwap) { + this.altValue = + new BigNumber(control.value).multipliedBy(this.coinSwapSelected.rate[this.fiatCode]); + } else { + this.altValue = + new BigNumber(control.value).multipliedBy(this.coinReceiveSelected.rate[this.fiatCode]); + } + if (this.altValue.isGreaterThan(0)) { + this.minWithCurrentFiat = + this.coinSwapSelected.min * this.usdRate[this.fiatCode]; + if (this.altValue.toNumber() < this.minWithCurrentFiat) { + return { amountMinValidator: true }; + } else { + return null; + } + } else { + return null; + } + }; + } + + getNameCoin(code) { + let nameCoin = ''; + const coin = this.currencyProvider.getCoin(code.toUpperCase()); + nameCoin = this.currencyProvider.getCoinName(coin) || ''; + return nameCoin; + } + + amountMaxValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.value === 0) { + return null; + } + if (this.altValue.isGreaterThan(0)) { + this.maxWithCurrentFiat = + this.coinReceiveSelected.max * this.usdRate[this.fiatCode]; + if (this.altValue.toNumber() > this.maxWithCurrentFiat) { + return { amountMaxValidator: true }; + } else { + return null; + } + } else { + return null; + } + }; + } + + + addressValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + // handle case no address input + if (!control.value) { + return null; + } + if (control.value.length === 0) { + return { addressNotInput: true }; + } + const addressInputValue = this.createForm.controls['address'].value; + // handle case token + if (this.coinReceiveSelected.isToken) { + try { + const { prefix, type, hash } = + this.addressProvider.decodeAddress(addressInputValue); + if (prefix === 'etoken' || prefix === 'ecash') { + return null; + } else { + return { addressInvalid: true }; + } + } catch (e) { + return { addressInvalid: true }; + } + } + + // handle case coin + const parsedData = this.incomingDataProvider.parseData(addressInputValue); + if ( + parsedData && + _.indexOf(this.validDataTypeMap, parsedData.type) != -1 + ) { + this.validAddress = this.checkCoinAndNetwork(addressInputValue, this.coinReceiveSelected.network); + if (this.validAddress) { + return null; + } else { + return { addressInvalid: true }; + } + } else { + return { addressInvalid: true }; + } + }; + } + + handleKeyDown(isSwap: boolean, event) { + const keyInput = event.key; + const pattern = /[^0-9\.]/; + const keyCode = event.keyCode; + const allowSpecialKeyCode = [37, 39, 8, 46]; + const swapValueInputStr = + this.createForm.controls['swapAmount'].value.toString(); + const receiveValueInputStr = + this.createForm.controls['receiveAmount'].value.toString(); + if (isSwap) { + if (keyInput === '.') { + if (swapValueInputStr.split('.').length > 1) { + event.preventDefault(); + } + } else if ( + !allowSpecialKeyCode.includes(keyCode) && + pattern.test(keyInput) + ) { + event.preventDefault(); + } + } else { + if (!allowSpecialKeyCode.includes(keyCode) && pattern.test(keyInput)) { + event.preventDefault(); + } + } + } + + handleSearchInput(){ + + if(this.searchValue.trim().length > 1){ + this.orderProvider.getOrderInfo(this.searchValue).then((res: IOrder) => { + this.router.navigate(['/order-swap'], { + replaceUrl: true, + state: { + order: res + } + }); + }).catch(e => { + this.showErrorInfoSheet(e); + }) + } + } + + formatAmountWithLimitDecimal(amount: number, maxDecimals): number { + if (amount.toString().split('.').length > 1) { + if (amount.toString().split('.')[1].length > maxDecimals) { + return Number( + amount.toString().split('.')[0] + + '.' + + amount.toString().split('.')[1].substr(0, maxDecimals) + ); + } + return amount; + } else { + return amount; + } + } + + formatInput(balance) { + if (typeof balance === 'string') balance = balance.replace(/,/g, ''); + if (isNaN(Number(balance)) || Number(balance) <= 0) { + return '0.00'; + } else { + if (Number(balance) < 10) { + return Number( + Number(balance).toFixed( + Math.round(1 / Number(balance)).toString().length + 2 + ) + ).toLocaleString('en-GB'); + } else { + return Number( + Number(balance).toFixed( + Math.round(1 / Number(balance)).toString().length + 1 + ) + ).toLocaleString('en-GB'); + } + } + } + + handleUpdateRate() { + this.rateProvider.updateRatesCustom().then(data => { + this.usdRate = data['eat']; + this.listConfig.coinSwap.forEach(coin => { + const code = coin.code.toLowerCase(); + coin.rate = data[code]; + }); + this.listConfig.coinReceive.forEach(coin => { + const code = coin.code.toLowerCase(); + coin.rate = data[code]; + }); + }); + } + + handleCoinSwapChange(event) { + const coinSwapCodeSelected = event.detail.value; + this.coinSwapSelected = this.listConfig.coinSwap.find( + s => s.code === coinSwapCodeSelected + ); + this.resetFormControl(); + } + + handleCoinReceiveChange(event) { + const coinReceiveCodeSelected = event.detail.value; + this.coinReceiveSelected = this.listConfig.coinReceive.find( + s => s.code === coinReceiveCodeSelected + ); + this.resetFormControl(); + } + + resetFormControl() { + this.createForm.controls['swapAmount'].setValue(0); + this.createForm.controls['receiveAmount'].setValue(0); + this.createForm.controls['address'].setValue(''); + } + + ionViewWillEnter() { + this.config = this.configProvider.get(); + this.fiatCode = this.config.wallet.settings.alternativeIsoCode; + this.currentTheme = + this.themeProvider.getCurrentAppTheme() === 'Dark Mode' + ? 'dark' + : 'light'; + this._cdRef.markForCheck(); + } + + private checkCoinAndNetwork(data, network): boolean { + let isValid, addrData; + + addrData = this.addressProvider.getCoinAndNetwork(data, network); + isValid = + this.currencyProvider + .getChain(this.coinReceiveSelected.code as Coin) + .toLowerCase() == addrData.coin && addrData.network == network; + + if (isValid) { + return true; + } else { + return false; + } + } + + public openSettingPage() { + this.router.navigate(['/setting']); + } + + public createOrder() { + const orderOpts = { + fromCoinCode: this.coinSwapSelected.code, + amountFrom: this.convertAmountToSatoshiAmount( + this.coinSwapSelected, + this.createForm.controls['swapAmount'].value as number + ), + isFromToken: this.coinSwapSelected.isToken, + fromTokenId: this.coinSwapSelected.isToken + ? this.coinSwapSelected.tokenInfo.id + : null, + toCoinCode: this.coinReceiveSelected.code, + toTokenId: this.coinReceiveSelected.isToken + ? this.coinReceiveSelected.tokenInfo.id + : null, + isToToken: this.coinReceiveSelected.isToken, + createdRate: + this.coinSwapSelected.rate.USD / this.coinReceiveSelected.rate.USD, + addressUserReceive: this.createForm.controls['address'].value, + toTokenInfo : this.coinReceiveSelected.tokenInfo || null, + fromTokenInfo : this.coinSwapSelected.tokenInfo || null, + fromNetwork: this.coinSwapSelected.network, + toNetwork: this.coinReceiveSelected.network + + } as OrderOpts; + this.orderProvider + .createOrder(orderOpts) + .then((result: IOrder) => { + this.router.navigate(['/order-swap'], { + state: { + orderId: result.id + } + }); + }).catch(e => { + this.showErrorInfoSheet(e); + }) + } + + public showErrorInfoSheet( + error: any, + title?: string, + exit?: boolean + ): void { + let msg: string; + if (!error) return; + // Currently the paypro error is the following string: 500 - "{}" + if (error.status === 500) { + msg = error.error.error; + } + + const infoSheetTitle = title ? title : this.translate.instant('Error'); + + this.errorsProvider.showDefaultError( + msg || this.bwcErrorProvider.msg(error), + infoSheetTitle, + () => { + } + ); + } +} diff --git a/src/app/pages/swap/order-swap/order-swap.component.html b/src/app/pages/swap/order-swap/order-swap.component.html new file mode 100644 index 00000000000..ee0b39dc0cc --- /dev/null +++ b/src/app/pages/swap/order-swap/order-swap.component.html @@ -0,0 +1,126 @@ + +
+
+ {{'Review the payment instructions carefully. Sending anything other than Bitcoin Cash to the specificed destination via the correct network can result in permanent loss of your finds.' | translate}} +
+ +
+

Order number: {{order.id.toString().slice(-10)}}

+

+ {{ labelStatusString }}

+
+
+ 1 {{ order.fromCoinCode.toUpperCase() }}   = +   {{ (1 * ( order.updatedRate || order.createdRate )).toFixed(2) }} + {{ order.toCoinCode.toUpperCase() }} +
+ + + +
+
+
+ {{'The final rate will be determined upon receipt of your {coin} payment.' | translate: {coin: order.fromCoinCode.toUpperCase()} }} +
+
+ +
+ + +
+

Send exactly: {{ order.amountFrom / order.fromSatUnit }} {{ order.fromCoinCode.toUpperCase() }}

+

You receive: + {{ order.amountFrom / order.fromSatUnit * ((order.updatedRate && order.updatedRate > 0) ? order.updatedRate : order.createdRate)}} + {{ order.toCoinCode.toUpperCase() }}

+
+
+

Send between

+

{{ minSwapAmount.toFixed(2) }} {{ order.fromCoinCode.toUpperCase() }}

+
+
+

and

+

{{ maxSwapAmount.toFixed(2) }} {{ order.fromCoinCode.toUpperCase() }}

+
+
+ +
+ +
+ + {{'You can send more than ' + maxSwapAmount.toFixed(2) + ' ' + order.fromCoinCode.toUpperCase() }} + + + {{ ', however we will manually review the order which might cause a delay.' | translate }} + +
+
+ +
+ +
+
+

You'll send

+

{{ order.amountFrom / order.fromSatUnit + ' ' + order.fromCoinCode.toUpperCase() }}

+
+
+

You'll receive

+

+ {{ ( order.amountFrom / order.fromSatUnit * (order.updatedRate || order.createdRate) ) + ' ' + order.toCoinCode.toUpperCase() }} +

+

{{ order.addressUserReceive.slice(-10) }}

+
+
+
+ +
+ +
+ +

TxId user deposit:

+ + + {{ txId }} + + +
+ + +

TxId user receive:

+ + + {{ txId }} + + +
+ +

{{ errorStr }}

+
+ +
+ + +
+ +
+
\ No newline at end of file diff --git a/src/app/pages/swap/order-swap/order-swap.component.scss b/src/app/pages/swap/order-swap/order-swap.component.scss new file mode 100644 index 00000000000..7fff68a1fda --- /dev/null +++ b/src/app/pages/swap/order-swap/order-swap.component.scss @@ -0,0 +1,247 @@ +page-order-swap { + p { + margin: 0; + } + + .warning-order-swap { + background: #FFDDB6; + color: #2B1700; + font-size: 12px; + letter-spacing: 0.4px; + line-height: 16px; + padding: 12px 16px; + border-radius: 8px; + } + + .order-swap-container { + background: #FAFAFB; + border: 1px solid rgba(126, 208, 255, 0.16); + border-radius: 16px; + padding: 1rem; + margin-top: 1rem; + .oder-number { + text-align: center; + color: rgba(0, 30, 46, 0.6); + font-size: 12px; + letter-spacing: 0.4px; + } + .status-oder { + text-align: center; + color: #CC8000; + letter-spacing: 0.1px; + font-weight: 600; + line-height: 20px; + margin: 1rem 0; + } + .status-completed { + color: #006C44 + } + .card-exchange-rate-oder { + padding: 8px 1rem; + border: 1px solid rgba(112, 129, 138, 0.16); + border-radius: 16px; + width: 56%; + margin: auto; + margin-bottom: 24px; + .count-down-card { + display: flex; + justify-content: center; + margin-bottom: 5px; + span { + color: #001E2E; + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + } + .count-down-container { + margin-left: 8px; + display: flex; + align-items: center; + .count-down { + font-weight: 600; + line-height: 16px; + text-align: center; + letter-spacing: 0.5px; + color: rgba(0, 30, 46, 0.6); + margin-left: 8px; + span { + font-size: 12px; + } + } + img { + position: absolute; + } + } + } + .sub-title { + font-size: 12px; + line-height: 16px; + text-align: center; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + } + .info-card-account { + margin-top: 24px; + display: flex; + ngx-qrcode { + margin-right: 20px; + width: 90px; + height: 90px; + img { + border-radius: 8px; + } + } + .coin-name { + margin-top: 5px; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + .address { + display: flex; + gap: 5px; + align-items: center; + margin: 5px 0; + .coin-address { + font-size: 16px; + line-height: 24px; + letter-spacing: 0.8px; + color: #001E2E; + } + ion-icon { + font-size: 23px; + color: #001E2E; + } + } + } + .card-amount-order { + .amount-container { + display: flex; + justify-content: space-between; + margin: 24px 0; + } + .start-amount { + flex: none; + order: 0; + flex-grow: 1; + .label-amount { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + .amount { + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: #001E2E; + } + } + .end-amount { + flex: none; + order: 2; + flex-grow: 1; + .label-amount { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + .amount { + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: #001E2E; + } + .address-received { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.6px; + color: rgba(0, 30, 46, 0.6); + } + } + .notify-content { + display: flex; + padding: 8px; + border: 1px solid rgba(112, 129, 138, 0.16); + border-radius: 16px; + margin-bottom: 24px; + ion-icon { + font-size: 32px; + color: rgba(0, 30, 46, 0.38); + } + .warning-message { + margin-left: 10px; + span { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + .title-message { + color: #CC8000; + } + } + } + } + } + + .list-user-deposit { + margin: 24px 0; + p { + margin-bottom: 8px; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + .user-deposit { + margin-bottom: 8px; + } + } + + .footer-order-swap { + margin-top: 2rem; + text-align: center; + p { + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: rgba(0, 30, 46, 0.6); + } + span { + font-weight: 400; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.4px; + color: #001E2E; + } + } + + .line-driver { + height: 1px; + background: rgba(112, 129, 138, 0.16); + } + + .order-swap-content { + margin-bottom: 1rem; + } + + @media screen and (max-width: 390px) { + .order-swap-content { + padding: 0 1rem; + } + } + + wide-header-page { + ion-content { + --padding-bottom: 0 !important; + } + } + +} \ No newline at end of file diff --git a/src/app/pages/swap/order-swap/order-swap.component.spec.ts b/src/app/pages/swap/order-swap/order-swap.component.spec.ts new file mode 100644 index 00000000000..638ddd0c120 --- /dev/null +++ b/src/app/pages/swap/order-swap/order-swap.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { OrderSwapComponent } from './order-swap.component'; + +describe('OrderSwapComponent', () => { + let component: OrderSwapComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ OrderSwapComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(OrderSwapComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/swap/order-swap/order-swap.component.ts b/src/app/pages/swap/order-swap/order-swap.component.ts new file mode 100644 index 00000000000..d0cbf3d4c91 --- /dev/null +++ b/src/app/pages/swap/order-swap/order-swap.component.ts @@ -0,0 +1,219 @@ +import { ChangeDetectorRef, Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Router } from '@angular/router'; +import { NavParams } from '@ionic/angular'; +import { CountdownComponent } from 'ngx-countdown'; +import { BwcErrorProvider, Coin, ConfigProvider, CurrencyProvider, ErrorsProvider, ExternalLinkProvider, OrderProvider } from 'src/app/providers'; +import { CoinConfig, TokenInfo } from '../config-swap'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; + +interface IOrder { + id: string | number; + version: number; + priority: number; + fromCoinCode: string; + fromTokenId?: string; + amountFrom: number; + fromSatUnit: number; + isFromToken: boolean; + toCoinCode: string; + isToToken: boolean; + toSatUnit: number; + amountSentToUser: number; + amountUserDeposit: number; + createdRate: number; + updatedRate: number; + addressUserReceive: string; + adddressUserDeposit: string; + toTokenId?: string; + listTxIdUserDeposit?: string[]; + listTxIdUserReceive?: string[]; + status?: string; + isSentToFund?: boolean; + isSentToUser?: boolean; + endedOn?: number; + createdOn?: number; + error?: string; + coinConfig?: CoinConfig; + toTokenInfo?: TokenInfo; + fromTokenInfo?: TokenInfo; + note?: string; + pendingReason?: string; + lastModified?: number; + isResolve?: boolean; +} + +@Component({ + selector: 'page-order-swap', + templateUrl: './order-swap.component.html', + styleUrls: ['./order-swap.component.scss'], + encapsulation: ViewEncapsulation.None, +}) +export class OrderSwapPage implements OnInit { + navPramss: any; + order: IOrder = null; + orderId: string = ''; + coinSwap: CoinConfig = null; + coinReceive: CoinConfig = null; + minSwapAmount = 0; + maxSwapAmount = 0; + errorStr = ''; + public createdDateStr = ''; + public endedDateStr = ''; + blockexplorerUrl = ''; + labelStatusString = ''; + @ViewChild('cd', { static: false }) private countdown: CountdownComponent; + constructor( private router: Router, + private navParams: NavParams, + private orderProvider: OrderProvider, + private location: Location, + private errorsProvider: ErrorsProvider, + private translate: TranslateService, + private _cdRef: ChangeDetectorRef, + private bwcErrorProvider: BwcErrorProvider, + private externalLinkProvider: ExternalLinkProvider, + private configProvider: ConfigProvider, + private currencyProvider: CurrencyProvider) { + if (this.router.getCurrentNavigation()) { + this.navPramss = this.router.getCurrentNavigation().extras.state; + } else { + this.navPramss = history ? history.state : {}; + } + if(this.navPramss.order){ + this.order = this.navPramss.order; + this.orderId = this.order.id as string; + } + if(this.navPramss.orderId){ + this.orderId = this.navPramss.orderId; + this.getOrderInfo(); + } + + } + + ngOnInit() { + + } + + getNameCoin(order: IOrder) { + let nameCoin = ''; + if (order && order.isFromToken) { + nameCoin = order.fromTokenInfo.name; + } else { + const coin = this.currencyProvider.getCoin(order.fromCoinCode.toUpperCase()); + nameCoin = this.currencyProvider.getCoinName(coin) || ''; + } + return nameCoin; + } + + back() { + this.router.navigate(['/'], { replaceUrl: true }); + } + + getLabelStatus(order: IOrder) { + if (order) { + let label = ''; + switch (order.status) { + case 'waiting': + label = `Waiting for ${order.toCoinCode.toUpperCase()} payment`; + break; + case 'pending': + label = `Order is pending for review`; + break; + case 'expired': + label = `Order is expired`; + break; + case 'complete': + label = `Order completed`; + break; + default: + break; + } + this.labelStatusString = label; + this._cdRef.markForCheck(); + } else{ + this.labelStatusString = ''; + } + } + + handleEvent(event){ + if(event.action === 'done'){ + this.countdown.restart(); + this.getOrderInfo(); + } + } + + getOrderInfo(){ + this.orderProvider.getOrderInfo(this.orderId).then((res: IOrder) => { + this.order = res; + this.coinReceive = this.order.coinConfig; + this.maxSwapAmount = this.coinReceive.maxConvertToSat / this.order.toSatUnit / ( this.order.updatedRate || this.order.createdRate ); + this.minSwapAmount = this.coinReceive.minConvertToSat / this.order.toSatUnit / ( this.order.updatedRate || this.order.createdRate ); + this.createdDateStr = new Date(this.order.createdOn).toUTCString(); + this.endedDateStr = new Date(this.order.endedOn).toUTCString(); + this.getLabelStatus(this.order); + this.getErrorStr(this.order); + this._cdRef.markForCheck(); + }).catch(e => { + this.showErrorInfoSheet(e); + }) + } + + public showErrorInfoSheet( + error: any, + title?: string, + exit?: boolean + ): void { + let msg: string; + if (!error) return; + // Currently the paypro error is the following string: 500 - "{}" + if (error.status === 500) { + msg = error.error.error; + } + + const infoSheetTitle = title ? title : this.translate.instant('Error'); + + this.errorsProvider.showDefaultError( + msg || this.bwcErrorProvider.msg(error), + infoSheetTitle, + () => { + + } + ); + } + public getErrorStr(order: IOrder){ + if(order){ + if(order.status !== 'complete' && order.error && order.error.length > 0){ + if(order.pendingReason === 'OUT_OF_FUND'){ + this.errorStr = 'This order need to be reviewed by admin'; + } else{ + this.errorStr = "Error: " + order.error; + } + } + else{ + this.errorStr = ''; + } + } + } + public viewOnBlockchain(coin, isToken, txId, network): void { + let defaults = this.configProvider.getDefaults(); + const coinSelected = isToken ? 'xec' : coin; + if(network === 'livenet') + this.blockexplorerUrl = defaults.blockExplorerUrl[coinSelected]; + else + this.blockexplorerUrl = defaults.blockExplorerUrlTestnet[coinSelected]; + const url = `https://${this.blockexplorerUrl}tx/${txId}`; + let optIn = true; + let title = null; + let message = this.translate.instant('View Transaction'); + let okText = this.translate.instant('Open'); + let cancelText = this.translate.instant('Go Back'); + this.externalLinkProvider.open( + url, + optIn, + title, + message, + okText, + cancelText + ); + } +} diff --git a/src/app/pages/token-details/token-details.html b/src/app/pages/token-details/token-details.html index 27fe76fe4b9..2ca7493ab77 100644 --- a/src/app/pages/token-details/token-details.html +++ b/src/app/pages/token-details/token-details.html @@ -25,7 +25,7 @@ - +
diff --git a/src/app/pages/token-details/token-details.ts b/src/app/pages/token-details/token-details.ts index a5f8b137041..88099678fa9 100644 --- a/src/app/pages/token-details/token-details.ts +++ b/src/app/pages/token-details/token-details.ts @@ -62,6 +62,8 @@ export class TokenDetailsPage { public isShowZeroState = false; private tokenId; public isSendFromHome: boolean = false; + public isGenNewAddress: boolean = false; + constructor( public http: HttpClient, private router: Router, @@ -121,6 +123,10 @@ export class TokenDetailsPage { } } + handleGenNewAddress(event){ + this.isGenNewAddress = event; + } + ionViewDidEnter() { setTimeout(() => { if (this.router.getCurrentNavigation()) { @@ -557,9 +563,13 @@ export class TokenDetailsPage { public handleNavigateBack() { if (this.isSendFromHome) { this.router.navigate(['/tabs/home']); + } else if(this.isGenNewAddress){ + this.isGenNewAddress = false; + this.router.navigate(['/tabs/home']).then(() => { + this.router.navigate(['/tabs/wallets']); + }) } else { this.router.navigate(['/tabs/wallets']); } } - } \ No newline at end of file diff --git a/src/app/pages/wallet-details/wallet-details.html b/src/app/pages/wallet-details/wallet-details.html index 69d80544274..f7ce8d00563 100644 --- a/src/app/pages/wallet-details/wallet-details.html +++ b/src/app/pages/wallet-details/wallet-details.html @@ -40,7 +40,7 @@
- +
diff --git a/src/app/pages/wallet-details/wallet-details.ts b/src/app/pages/wallet-details/wallet-details.ts index a5ba1114ba2..5cbeca51596 100644 --- a/src/app/pages/wallet-details/wallet-details.ts +++ b/src/app/pages/wallet-details/wallet-details.ts @@ -82,6 +82,7 @@ export class WalletDetailsPage { public finishParam: any; public isScroll = false; public isSendFromHome: boolean = false; + public isGenNewAddress: boolean = false; toast?: HTMLIonToastElement; typeErrorQr = NgxQrcodeErrorCorrectionLevels; @@ -173,6 +174,10 @@ export class WalletDetailsPage { this.blockexplorerUrlTestnet = defaults.blockExplorerUrlTestnet[this.wallet.coin]; } + handleGenNewAddress(event){ + this.isGenNewAddress = event; + } + async handleScrolling(event) { if (event.detail.currentY > 0) { this.isScroll = true; @@ -1023,6 +1028,11 @@ export class WalletDetailsPage { public handleNavigateBack() { if (this.isSendFromHome) { this.router.navigate(['/tabs/home']); + } else if(this.isGenNewAddress){ + this.isGenNewAddress = false; + this.router.navigate(['/tabs/home']).then(() => { + this.router.navigate(['/tabs/wallets']); + }) } else { this.router.navigate(['/tabs/wallets']); } diff --git a/src/app/providers/feature-flags.service.ts b/src/app/providers/feature-flags.service.ts new file mode 100644 index 00000000000..b42490d214c --- /dev/null +++ b/src/app/providers/feature-flags.service.ts @@ -0,0 +1,62 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { truncateSync } from "fs"; +import { get, has, truncate } from "lodash"; +import { of } from "rxjs"; +import { tap } from "rxjs/operators"; +import { Router } from '@angular/router'; +import { CreateSwapPage } from "../pages/swap/create-swap/create-swap.component"; +import { OrderSwapPage } from "../pages/swap/order-swap/order-swap.component"; +import { SettingsPage } from "../pages/settings/settings"; +import env from 'src/environments'; + +export interface FeatureConfig { + abcpay: boolean, + swap: boolean +} + +@Injectable({ + providedIn: "root" + }) + export class FeatureFlagsService { + config: FeatureConfig = null; + configUrl = ``; // <-- URL for getting the config + + constructor(private http: HttpClient, private Router: Router) {} + + /** + * We convert it to promise so that this function can + * be called by the APP_INITIALIZER + */ + loadConfig() : Promise { + let buildSwapAlone = false; + return of({ + abcpay: true, + swap: true + } as FeatureConfig).pipe(tap(data =>{ + if(data.abcpay && data.swap){ + if(env.buildSwapALone){ + buildSwapAlone = true; + } + } + if(buildSwapAlone || !data.abcpay && data.swap){ + const routes = this.Router.config; + routes.shift(); + routes.unshift({ path: '', component: CreateSwapPage }); + const indexPath = routes.findIndex(r => r.path === 'create-swap'); + routes.splice(indexPath, 1); + routes.push({ path: 'order-swap', component: OrderSwapPage }); + this.Router.resetConfig(routes); + } + this.config = data; + } )) + .toPromise();; + } + + isFeatureEnabled(key: string) { + if (this.config && has(this.config, key)) { + return get(this.config, key, false); + } + return false; + } + } \ No newline at end of file diff --git a/src/app/providers/feature-gaurd.service.ts b/src/app/providers/feature-gaurd.service.ts new file mode 100644 index 00000000000..695a18f0e84 --- /dev/null +++ b/src/app/providers/feature-gaurd.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanDeactivate, CanLoad, Route, Router, RouterStateSnapshot, UrlSegment, UrlTree } from '@angular/router'; +import { Observable } from 'rxjs'; +import { FeatureFlagsService } from './feature-flags.service'; + +@Injectable({ + providedIn: 'root', +}) +export class FeatureGuard implements CanActivate, CanLoad { + constructor( + private featureFlagsService: FeatureFlagsService, + private router: Router + ) {} + canLoad(route: Route, segments: UrlSegment[]): boolean | UrlTree | Observable | Promise { + const { + data: { feature }, // <-- Get the module name from route data + } = route; + if (feature) { + const isEnabled = this.featureFlagsService.isFeatureEnabled(feature); + if (isEnabled) { + return true; + } + } + this.router.navigate(['/']); + return false; + } + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise { + const { + data: { feature }, // <-- Get the module name from route data + } = route; + if (feature) { + const isEnabled = this.featureFlagsService.isFeatureEnabled(feature); + if (isEnabled) { + return true; + } + } + this.router.navigate(['/']); + return false; + } +// canActivate( +// route: Route, +// segments: UrlSegment[] +// ): +// boolean { +// const { +// data: { feature }, // <-- Get the module name from route data +// } = route; +// if (feature) { +// const isEnabled = this.featureFlagsService.isFeatureEnabled(feature); +// if (isEnabled) { +// return true; +// } +// } +// this.router.navigate(['/swap']); +// return false; +// } + + CanDeactivate( + route: Route, + segments: UrlSegment[] + ): + | Observable + | Promise + | boolean + | UrlTree { + const { + data: { feature }, // <-- Get the module name from route data + } = route; + if (feature) { + const isEnabled = this.featureFlagsService.isFeatureEnabled(feature); + if (isEnabled) { + return true; + } + } + this.router.navigate(['/swap']); + return false; + } +} \ No newline at end of file diff --git a/src/app/providers/index.ts b/src/app/providers/index.ts index f15edb985b5..5f792bac85d 100644 --- a/src/app/providers/index.ts +++ b/src/app/providers/index.ts @@ -58,6 +58,7 @@ export { ProfileProvider } from './profile/profile'; export { PushNotificationsProvider } from './push-notifications/push-notifications'; export { RateProvider } from './rate/rate'; export { LixiLotusProvider } from './lixi-lotus/lixi-lotus'; +export { OrderProvider } from './order/order-provider'; export { ReplaceParametersProvider } from './replace-parameters/replace-parameters'; export { ScanProvider } from './scan/scan'; export { ThemeProvider } from './theme/theme'; diff --git a/src/app/providers/order/order-provider.ts b/src/app/providers/order/order-provider.ts new file mode 100644 index 00000000000..d75c22c68e1 --- /dev/null +++ b/src/app/providers/order/order-provider.ts @@ -0,0 +1,64 @@ +import { HttpClient, HttpErrorResponse, HttpResponse } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { reject } from "lodash"; +import { throwError } from "rxjs"; +import { catchError } from "rxjs/operators"; +import { ConfigSwap } from "src/app/pages/swap/config-swap"; +import { ConfigProvider } from "../config/config"; +import { Logger } from "../logger/logger"; + +@Injectable({ + providedIn: 'root' + }) + export class OrderProvider { + private bwsURL: string; + + + constructor( + private logger: Logger, + private configProvider: ConfigProvider, + private http: HttpClient + ) { + this.logger.debug('LixiLotusProvider initialized'); + const defaults = this.configProvider.getDefaults(); + this.bwsURL = defaults.bws.url; + } + + public getTokenInfo(): Promise { + return new Promise(resolve =>{ + this.http.get(`${this.bwsURL}/v3/tokenInfo/`).subscribe(res =>{ + resolve(res); + }); + }); + } + + public getConfigSwap(): Promise { + return new Promise(resolve =>{ + this.http.get(`${this.bwsURL}/v3/configSwap/`).subscribe(res =>{ + resolve(ConfigSwap.fromObj(res)); + }); + }); + } + + public getOrderInfo(orderId): Promise { + return this.http.get(`${this.bwsURL}/v3/order/${orderId}`).toPromise(); + } + + public createOrder(orderOpts): Promise { + return this.http.post(`${this.bwsURL}/v3/order/create/`, orderOpts).toPromise(); + } + + private handleError(error: HttpErrorResponse) { + if (error.status === 0) { + // A client-side or network error occurred. Handle it accordingly. + console.error('An error occurred:', error.error); + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong. + console.error( + `Backend returned code ${error.status}, body was: `, error.error); + } + // Return an observable with a user-facing error message. + return throwError(() => new Error('Something bad happened; please try again later.')); + } + } \ No newline at end of file diff --git a/src/app/providers/providers.module.ts b/src/app/providers/providers.module.ts index 0e4fa7562e2..f2afdefbb50 100644 --- a/src/app/providers/providers.module.ts +++ b/src/app/providers/providers.module.ts @@ -53,6 +53,7 @@ import { QRScanner, RateProvider, LixiLotusProvider, + OrderProvider, TokenProvider, ReleaseProvider, ReplaceParametersProvider, @@ -72,7 +73,7 @@ import { CustomErrorHandler, ThemeDetection, RedirectGuard, - PreviousRouteService + PreviousRouteService, } from './index'; @NgModule({ @@ -122,6 +123,7 @@ import { PushNotificationsProvider, RateProvider, LixiLotusProvider, + OrderProvider, TokenProvider, ReplaceParametersProvider, ReleaseProvider, diff --git a/src/app/providers/rate/rate.ts b/src/app/providers/rate/rate.ts index 4f2f67616a9..7561d894b73 100644 --- a/src/app/providers/rate/rate.ts +++ b/src/app/providers/rate/rate.ts @@ -97,7 +97,7 @@ export class RateProvider { this.rates[coin] = !_.isEmpty(coinRates) ? coinRates : { USD: 0 }; this.ratesAvailable[coin] = true; }); - resolve(undefined); + resolve(this.rates); }) .catch(err => { this.logger.error(err); @@ -107,6 +107,36 @@ export class RateProvider { }); } + public updateRatesCustom(chain?: string): Promise { + let rateList= []; + for (const coin of this.currencyProvider.getAvailableCoins()) { + rateList[coin.toLowerCase()] = { USD: 0 }; + } + return new Promise((resolve, reject) => { + this.getRates() + .then(res => { + _.map(res, (rates, coin) => { + const coinRates = {}; + _.each(rates, r => { + const rate = { [r.code]: r.rate }; + Object.assign(coinRates, rate); + + // set alternative currency list + if (r.code && r.name) { + this.alternatives[r.code] = { name: r.name }; + } + }); + rateList[coin.toLowerCase()] = !_.isEmpty(coinRates) ? coinRates : { USD: 0 }; + }); + resolve(rateList); + }) + .catch(err => { + this.logger.error(err); + reject(err); + }); + }); + } + public getRates(): Promise { return new Promise(resolve => { this.http.get(`${this.bwsURL}/v3/fiatrates/`).subscribe(res => { diff --git a/src/app/swap/swap-routing.module.ts b/src/app/swap/swap-routing.module.ts new file mode 100644 index 00000000000..3f051a79274 --- /dev/null +++ b/src/app/swap/swap-routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CreateSwapPage } from '../pages/swap/create-swap/create-swap.component'; +import { OrderSwapPage } from '../pages/swap/order-swap/order-swap.component'; + +const routes: Routes = [ + { + path: 'swap', + children: [ + { + path: 'create', + component: CreateSwapPage + }, + { + path: 'order', + component: OrderSwapPage + }, + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], +}) +export class SwapPageRoutingModule {} diff --git a/src/app/swap/swap.module.ts b/src/app/swap/swap.module.ts new file mode 100644 index 00000000000..9f7df1f2a73 --- /dev/null +++ b/src/app/swap/swap.module.ts @@ -0,0 +1,18 @@ +import { IonicModule } from '@ionic/angular'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { SwapPageRoutingModule } from './swap-routing.module'; + + +@NgModule({ + declarations: [], + imports: [ + IonicModule, + CommonModule, + FormsModule, + SwapPageRoutingModule + ] +}) +export class SwapModule { } diff --git a/src/assets/img/countdown-ico.svg b/src/assets/img/countdown-ico.svg new file mode 100644 index 00000000000..b66360557b6 --- /dev/null +++ b/src/assets/img/countdown-ico.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/swap/exchange-count-down.svg b/src/assets/img/swap/exchange-count-down.svg new file mode 100644 index 00000000000..b66360557b6 --- /dev/null +++ b/src/assets/img/swap/exchange-count-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/img/swap/exchange-sign-dark.svg b/src/assets/img/swap/exchange-sign-dark.svg new file mode 100644 index 00000000000..b0cac1d3ba2 --- /dev/null +++ b/src/assets/img/swap/exchange-sign-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/img/swap/exchange-sign-light.svg b/src/assets/img/swap/exchange-sign-light.svg new file mode 100644 index 00000000000..df1055846bf --- /dev/null +++ b/src/assets/img/swap/exchange-sign-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/theme/dark.scss b/src/theme/dark.scss index e2af6e5db3e..170c52bf7cf 100644 --- a/src/theme/dark.scss +++ b/src/theme/dark.scss @@ -394,6 +394,21 @@ } } + .mat-select { + .mat-select-arrow { + color: rgba(237, 239, 240, 0.6) !important; + } + } + + mat-select-trigger { + .mat-coin-name { + color: #EDEFF0 !important; + } + .mat-symbol-name { + color: rgba(237, 239, 240, 0.6) !important; + } +} + .swiper-pagination-bullet-active { background-color: var(--swiper-pagination-bullet-active-dark); } @@ -3724,4 +3739,130 @@ } } } + + page-create-swap { + .swap-exchange-container { + background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E; + border-color: #01293d; + .title { + color: var(--text-color-dark-theme); + } + ion-item { + --background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E; + } + .hint { + color: rgba(224, 228, 230, 0.6); + } + } + .swap-exchange-rate-container { + span { + color: var(--text-color-dark-theme); + } + countdown { + color: var(--text-color-dark-theme); + } + } + .address-container { + background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E; + border-color: #01293d; + } + .address-container { + p { + color: var(--text-color-dark-theme); + } + } + .search-container { + --background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E !important; + background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E !important; + border-color: rgba(112, 129, 138, 0.38) !important; + ion-icon { + color: rgba(237, 239, 240, 0.6) !important; + } + ion-input { + --color: var(--text-color-dark-theme) !important; + --placeholder-color: var(--text-color-dark-theme) !important; + } + } + .label-network { + color: rgba(224, 228, 230, 0.6) !important; + } + .header-title { + color: var(--text-color-dark-theme) !important; + } + } + + page-order-swap { + .info-card-account { + .coin-name { + color: rgba(224, 228, 230, 0.6) !important; + } + .address { + .coin-address { + color: var(--text-color-dark-theme) !important; + } + ion-icon { + color: var(--text-color-dark-theme) !important; + } + } + } + .order-swap-container { + background: linear-gradient(0deg, rgba(126, 208, 255, 0.11), rgba(126, 208, 255, 0.11)), #001E2E !important; + border-color: rgba(0, 101, 141, 0.08) !important; + .oder-number { + color: rgba(224, 228, 230, 0.6) !important; + } + .status-oder { + color: #FFB85D !important; + } + .status-completed { + color: #73DAA5 !important; + } + .card-exchange-rate-oder { + .count-down-card { + span { + color: var(--text-color-dark-theme) !important; + } + countdown { + color: var(--text-color-dark-theme) !important; + } + } + .sub-title { + color: rgba(224, 228, 230, 0.6) !important; + } + } + } + .card-amount-order { + .label-amount { + color: rgba(224, 228, 230, 0.6) !important; + } + .amount { + color: var(--text-color-dark-theme) !important; + } + .address-received { + color: rgba(224, 228, 230, 0.6) !important; + } + } + .notify-content { + ion-icon { + color: rgba(224, 228, 230, 0.38) !important; + } + .title-message { + color: #FFB85D !important; + } + span { + color: rgba(224, 228, 230, 0.6) !important; + } + } + .line-driver { + background: rgba(112, 129, 138, 0.38) !important; + } + .footer-order-swap { + p { + color: rgba(224, 228, 230, 0.6) !important; + span { + color: var(--text-color-dark-theme) !important; + } + } + } + } }