...
 
Commits (2)
......@@ -24,6 +24,7 @@ import { OffchainPaymentService } from './offchain-payment.service';
import { Client } from '../../services/api/client';
import { MarketingModule } from '../marketing/marketing.module';
import { BlockchainMarketingModule } from './marketing/marketing.module';
import { GetMetamaskComponent } from './metamask/getmetamask.component';
const cryptoRoutes: Routes = [
{
......@@ -52,6 +53,7 @@ const cryptoRoutes: Routes = [
BlockchainWalletAddressNoticeComponent,
TransactionOverlayComponent,
BlockchainTdeBuyComponent,
GetMetamaskComponent,
],
providers: [
TransactionOverlayService,
......@@ -100,7 +102,8 @@ const cryptoRoutes: Routes = [
BlockchainWalletSelector,
BlockchainWalletAddressNoticeComponent,
TransactionOverlayComponent,
BlockchainTdeBuyComponent
BlockchainTdeBuyComponent,
GetMetamaskComponent,
],
entryComponents: [
BlockchainTdeBuyComponent,
......
......@@ -39,6 +39,10 @@ export class LocalWalletService {
return cb(null, accounts);
}
async setupMetamask() {
return await this.transactionOverlay.waitForSetupMetaMask();
}
async unlock() {
if (this.account && this.privateKey) {
return Promise.resolve(true);
......@@ -46,7 +50,7 @@ export class LocalWalletService {
try {
const { privateKey, secureMode } = await this.transactionOverlay.waitForAccountUnlock(),
account = ethAccount.privateToAccount(privateKey).address;
account = ethAccount.privateToAccount(privateKey).address;
this.privateKey = privateKey;
this.account = account;
......
......@@ -35,9 +35,14 @@
You can not purchase more than $50 worth of ETH
</p>
<button class="m-btn m-btn--slim m-btn--action" (click)="buy()">
<button class="m-btn m-btn--slim m-btn--action" (click)="buy()" *ngIf="hasMetamask">
Buy with SendWyre
</button>
<a href="https://metamask.io/" target="_blank" *ngIf="!hasMetamask">
<button class="m-btn m-btn--slim m-btn--action">Download Metamask</button>
</a>
</div>
</m-modal>
......@@ -31,9 +31,21 @@ export class BlockchainEthModalComponent implements OnInit {
error: string = '';
usd: number = 40;
hasMetamask: boolean = true; // True by default
constructor(
private web3Wallet: Web3WalletService,
private cd: ChangeDetectorRef,
) { }
ngOnInit() {
// grab latest ETH rate
this.detectMetamask();
}
async detectMetamask() {
this.hasMetamask = !await this.web3Wallet.isLocal();
this.detectChanges();
}
get ethRate(): number {
......@@ -51,14 +63,30 @@ export class BlockchainEthModalComponent implements OnInit {
this.usd = eth * this.ethRate;
}
buy() {
async buy() {
this.error = '';
this.detectChanges();
if (!this.hasMetamask) {
this.error = 'You need to install metamask';
this.detectChanges();
return;
}
if (this.usd > 40) {
this.usd = 40;
this.error = 'You can not purchase more than $40';
this.detectChanges();
return;
}
let win = window.open('/checkout?usd=' + this.usd);
this.close.next(true);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<h2 class="m-get-metamask--title">
Setup Your OnChain Address to buy, send and receive crypto
</h2>
<div class="mdl-grid mdl-grid--no-spacing">
<div class="mdl-cell mdl-cell--4-col m-get-metamask--logo-container">
<img class="m-get-metamask--logo" [src]="minds.cdn_assets_url + 'assets/ext/metamask.png'" />
</div>
<div class="mdl-cell mdl-cell--4-col">
<h4>MetaMask</h4>
<p>Minds recommends using MetaMask for the most seamless user experience.</p>
<p class="m-get-metamask--button">
<a href="https://metamask.io/" target="_blank">
<button class="m-btn m-btn--slim m-btn--action">Download Metamask</button>
</a>
</p>
<p>
Be sure to refresh your browser after installing the plugin.
You can also
<a class="m-get-metamask--create-link" (click)="create()">create</a> or
<a class="m-get-metamask--provide-link" (click)="unlock()">provide</a> your own existing address.
</p>
<p class="m-get-metamask--button">
<button class="m-get-metamask--cancel-btn m-btn" (click)="cancel()">Cancel</button>
</p>
</div>
</div>
m-get-metamask {
padding: 16px;
max-width: 100%;
box-sizing: border-box;
display: block;
.m-get-metamask--title {
font-size: 42px;
margin: 0;
margin-bottom: 16px;
font-weight: bold;
line-height: 42px;
@include m-theme(){
color: themed($m-grey-800);
}
@media screen and (max-width: $max-mobile) {
font-size: 24px;
line-height: 1.2;
}
}
h4 {
font-weight: 600;
}
.m-get-metamask--logo-container {
display: flex;
justify-content: center;
align-content: center;
flex-direction: row;
.m-get-metamask--logo {
width: 227px;
height: 227px;
@media screen and (max-width: $max-mobile) {
width: 100px;
height: 100px;
}
}
}
.m-get-metamask--button {
text-align: left;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { GetMetamaskComponent } from './getmetamask.component';
describe('GetMetamaskComponent', () => {
let comp: GetMetamaskComponent;
let fixture: ComponentFixture<GetMetamaskComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [GetMetamaskComponent]
}).compileComponents();
}));
beforeEach((done) => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 2;
fixture = TestBed.createComponent(GetMetamaskComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable()
.then(() => {
fixture.detectChanges();
done();
});
}
});
it('should have a title', () => {
const title = fixture.debugElement.query(By.css('.m-get-metamask--title'));
expect(title).not.toBeNull();
});
it('should raise a create address event', () => {
spyOn(comp.actioned, 'emit').and.callThrough();
const link = fixture.debugElement.query(By.css('.m-get-metamask--create-link'));
link.nativeElement.click();
expect(comp.actioned.emit).toHaveBeenCalledWith(GetMetamaskComponent.ACTION_CREATE);
});
it('should raise an provide address event', () => {
spyOn(comp.actioned, 'emit').and.callThrough();
const link = fixture.debugElement.query(By.css('.m-get-metamask--provide-link'));
link.nativeElement.click();
expect(comp.actioned.emit).toHaveBeenCalledWith(GetMetamaskComponent.ACTION_UNLOCK);
});
it('should raise a cancel', () => {
spyOn(comp.actioned, 'emit').and.callThrough();
const link = fixture.debugElement.query(By.css('.m-get-metamask--cancel-btn'));
link.nativeElement.click();
expect(comp.actioned.emit).toHaveBeenCalledWith(GetMetamaskComponent.ACTION_CANCEL);
});
});
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'm-get-metamask',
templateUrl: 'getmetamask.component.html'
})
export class GetMetamaskComponent {
public static ACTION_CREATE = 'create';
public static ACTION_UNLOCK = 'unlock';
public static ACTION_CANCEL = 'cancel';
@Output() actioned: EventEmitter<String> = new EventEmitter();
minds: Minds = window.Minds;
create() {
this.actioned.emit(GetMetamaskComponent.ACTION_CREATE);
}
unlock() {
this.actioned.emit(GetMetamaskComponent.ACTION_UNLOCK);
}
cancel() {
this.actioned.emit(GetMetamaskComponent.ACTION_CANCEL);
}
}
<div class="m--blockchain--transaction-overlay">
<div class="m--blockchain--transaction-overlay--content">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" class="m--blockchain--transaction-overlay--logo" />
<h2 class="m--blockchain--transaction-overlay--title" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__COMPLETE_YOUR_TX">
Complete your OnChain transaction
</h2>
<ng-container *ngIf="comp !== COMP_SETUP_METAMASK">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" class="m--blockchain--transaction-overlay--logo" />
<h2 class="m--blockchain--transaction-overlay--title" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__COMPLETE_YOUR_TX" *ngIf="comp !== COMP_SETUP_METAMASK">
Complete your OnChain transaction
</h2>
<p class="m--blockchain--transaction-overlay--note" *ngIf="comp === COMP_METAMASK" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__COMPLETE_USING_METAMASK">
Please open your Metamask client to complete the transaction.
</p>
<p class="m--blockchain--transaction-overlay--note" *ngIf="comp === COMP_METAMASK" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__COMPLETE_USING_METAMASK">
Please open your Metamask client to complete the transaction.
</p>
<p class="m--blockchain--transaction-overlay--note" *ngIf="comp !== COMP_UNLOCK && !!message" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__YOURE_APPROVING_MESSAGE">
You are approving a {{ message }}
</p>
</ng-container>
<p class="m--blockchain--transaction-overlay--note" *ngIf="comp !== COMP_UNLOCK && !!message" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__YOURE_APPROVING_MESSAGE">
You are approving a {{ message }}
</p>
<ng-container *ngIf="comp === COMP_SETUP_METAMASK">
<m-get-metamask (actioned)="handleMetamaskAction($event)"></m-get-metamask>
</ng-container>
<ng-container *ngIf="comp === COMP_UNLOCK">
<p class="m--blockchain--transaction-overlay--note" i18n="@@M__BLOCKCHAIN__TX_OVERLAY__ENTER_PKEY_OR_DROP_CSV">
......
......@@ -10,6 +10,7 @@ import { FormsModule } from '@angular/forms';
import { MaterialSwitchMock } from '../../../../tests/material-switch-mock.spec';
import { web3WalletServiceMock } from '../../../../tests/web3-wallet-service-mock.spec';
import { Web3WalletService } from '../web3-wallet.service';
import { GetMetamaskComponent } from '../metamask/getmetamask.component';
describe('TransactionOverlayComponent', () => {
......@@ -19,8 +20,8 @@ describe('TransactionOverlayComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MaterialSwitchMock, TransactionOverlayComponent],
imports: [RouterTestingModule, FormsModule],
declarations: [MaterialSwitchMock, TransactionOverlayComponent, GetMetamaskComponent],
imports: [RouterTestingModule, FormsModule ],
providers: [
{ provide: TransactionOverlayService, useValue: transactionOverlayService },
{ provide: TokenContractService, useValue: TokenContractService },
......@@ -58,4 +59,12 @@ describe('TransactionOverlayComponent', () => {
expect(havingIssues.nativeElement.href).toMatch(/https?:\/\/.*\/token/);
});
it('should display get metamask component', () => {
comp.comp = comp.COMP_SETUP_METAMASK;
comp.detectChanges();
fixture.detectChanges();
const note: DebugElement = fixture.debugElement.query(By.css('.m-get-metamask--title'));
expect(note.nativeElement.textContent.trim()).toContain('Setup Your OnChain Address to buy, send and receive crypto');
});
});
......@@ -15,6 +15,7 @@ import { TransactionOverlayService } from './transaction-overlay.service';
import { TokenContractService } from '../contracts/token-contract.service';
import { Router } from '@angular/router';
import { Web3WalletService } from '../web3-wallet.service';
import { GetMetamaskComponent } from '../metamask/getmetamask.component';
@Component({
moduleId: module.id,
......@@ -49,6 +50,7 @@ export class TransactionOverlayComponent implements OnInit {
readonly COMP_METAMASK = 1;
readonly COMP_LOCAL = 2;
readonly COMP_UNLOCK = 3;
readonly COMP_SETUP_METAMASK = 4;
constructor(
protected service: TransactionOverlayService,
......@@ -335,4 +337,13 @@ export class TransactionOverlayComponent implements OnInit {
this.cd.markForCheck();
this.cd.detectChanges();
}
handleMetamaskAction($event) {
let next = $event;
if ($event === GetMetamaskComponent.ACTION_CANCEL) {
next = false;
}
this.eventEmitter.next($event);
this.hide();
}
}
......@@ -10,6 +10,21 @@ export class TransactionOverlayService {
this.comp = comp;
}
waitForSetupMetaMask(): Promise<string> {
const compEventEmitter = this.comp.show(this.comp.COMP_SETUP_METAMASK);
return new Promise((resolve, reject) => {
const subscription: Subscription = compEventEmitter.subscribe(data => {
subscription.unsubscribe();
if (data && !(data instanceof Error)) {
resolve(data);
} else {
reject((data && data.message) || 'User cancelled');
}
});
});
}
waitForAccountUnlock(): Promise<{ privateKey, account, secureMode }> {
let compEventEmitter = this.comp.show(this.comp.COMP_UNLOCK);
......
......@@ -21,7 +21,7 @@ export class Web3WalletService {
constructor(
protected localWallet: LocalWalletService,
protected transactionOverlay: TransactionOverlayService
protected transactionOverlay: TransactionOverlayService,
) { }
// Wallet
......@@ -77,6 +77,12 @@ export class Web3WalletService {
return this.local;
}
async setupMetamask() {
if (await this.isLocal()) {
return await this.localWallet.setupMetamask();
}
}
async unlock() {
if ((await this.isLocal()) && (await this.isLocked())) {
await this.localWallet.unlock();
......
......@@ -28,6 +28,7 @@ import { TransactionOverlayService } from '../../blockchain/transaction-overlay/
import { localWalletServiceMock } from '../../../../tests/local-wallet-service-mock.spec';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { Session } from '../../../services/session';
import { RouterTestingModule } from '@angular/router/testing';
/* tslint:disable */
@Component({
......@@ -111,6 +112,7 @@ let web3WalletServiceMock = new function () {
this.onChainInterfaceLabel = 'Metamask';
this.unavailable = false;
this.locked = false;
this.localWallet = false;
this.isUnavailable = jasmine.createSpy('isUnavailable').and.callFake(() => {
return this.unavailable;
......@@ -134,6 +136,11 @@ let web3WalletServiceMock = new function () {
return this.balance;
});
this.isLocal = jasmine.createSpy('isLocal').and.callFake(async () => {
return this.isLocalWallet;
});
this.getOnChainInterfaceLabel = jasmine.createSpy('getOnChainInterfaceLabel').and.callFake(() => {
return this.onChainInterfaceLabel ? this.onChainInterfaceLabel: 'Metamask';
});
......@@ -368,7 +375,7 @@ describe('BoostCreatorComponent', () => {
BoostP2PSearchMock,
BoostCheckoutMock,
],
imports: [FormsModule],
imports: [FormsModule, RouterTestingModule],
providers: [
{ provide: Session, useValue: sessionMock },
{ provide: Client, useValue: clientMock },
......
......@@ -9,7 +9,8 @@ import { TokenContractService } from '../../blockchain/contracts/token-contract.
import { BoostContractService } from '../../blockchain/contracts/boost-contract.service';
import { Web3WalletService } from '../../blockchain/web3-wallet.service';
import { OffchainPaymentService } from '../../blockchain/offchain-payment.service';
import { GetMetamaskComponent } from '../../blockchain/metamask/getmetamask.component';
import { Router } from '@angular/router';
type CurrencyType = 'offchain' | 'usd' | 'onchain' | 'creditcard';
export type BoostType = 'p2p' | 'newsfeed' | 'content';
......@@ -112,7 +113,8 @@ export class BoostCreatorComponent implements AfterViewInit {
private tokensContract: TokenContractService,
private boostContract: BoostContractService,
private web3Wallet: Web3WalletService,
private offchainPayment: OffchainPaymentService
private offchainPayment: OffchainPaymentService,
protected router: Router,
) { }
ngOnInit() {
......@@ -554,7 +556,22 @@ export class BoostCreatorComponent implements AfterViewInit {
if (this.web3Wallet.isUnavailable()) {
throw new Error('No Ethereum wallets available on your browser.');
} else if (!(await this.web3Wallet.unlock())) {
}
if (await this.web3Wallet.isLocal()) {
const action = await this.web3Wallet.setupMetamask();
switch (action) {
case GetMetamaskComponent.ACTION_CREATE:
this.router.navigate(['/wallet']);
this.inProgress = false;
this.overlayModal.dismiss();
break;
case GetMetamaskComponent.ACTION_CANCEL:
return;
}
}
if (!(await this.web3Wallet.unlock())) {
throw new Error('Your Ethereum wallet is locked or connected to another network.');
}
......
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import { WalletBalanceTokensComponent } from './balance.component';
import { TokenPipe } from '../../../../common/pipes/token.pipe';
......@@ -12,7 +13,8 @@ import { DebugElement } from '@angular/core';
import { Session } from '../../../../services/session';
import { sessionMock } from '../../../../../tests/session-mock.spec';
fdescribe('WalletBalanceTokensComponent', () => {
describe('WalletBalanceTokensComponent', () => {
let comp: WalletBalanceTokensComponent;
let fixture: ComponentFixture<WalletBalanceTokensComponent>;
......@@ -44,6 +46,9 @@ fdescribe('WalletBalanceTokensComponent', () => {
this.isLocal = jasmine.createSpy('getCurrentWallet').and.callFake(async () => {
return false;
});
this.getBalance = jasmine.createSpy('getBalance').and.callFake(async() => {
return 0;
});
}
const Web3WalletLocalServiceMock = new function () {
......@@ -64,6 +69,7 @@ fdescribe('WalletBalanceTokensComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ RouterTestingModule ],
declarations: [
TokenPipe,
TooltipComponentMock,
......
......@@ -6,6 +6,7 @@ import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { WireService } from '../wire.service';
import { Web3WalletService } from '../../blockchain/web3-wallet.service';
import { GetMetamaskComponent } from '../../blockchain/metamask/getmetamask.component';
import { TokenContractService } from '../../blockchain/contracts/token-contract.service';
import { MindsUser } from '../../../interfaces/entities';
import { Router } from '@angular/router';
......@@ -481,6 +482,19 @@ export class WireCreatorComponent {
this.submitted = true;
this.error = '';
if (await this.web3Wallet.isLocal()) {
const action = await this.web3Wallet.setupMetamask();
switch (action) {
case GetMetamaskComponent.ACTION_CREATE:
this.router.navigate(['/wallet']);
this.inProgress = false;
this.overlayModal.dismiss();
break;
case GetMetamaskComponent.ACTION_CANCEL:
return;
}
}
let { done } = await this.wireService.submitWire(this.wire);
if (done) {
......
......@@ -7,6 +7,7 @@ import { Session } from '../../../services/session';
import { WireService } from '../wire.service';
import { Web3WalletService } from '../../blockchain/web3-wallet.service';
import { TokenContractService } from '../../blockchain/contracts/token-contract.service';
import { GetMetamaskComponent } from '../../blockchain/metamask/getmetamask.component';
import { MindsUser } from '../../../interfaces/entities';
import { Router } from '@angular/router';
......@@ -371,6 +372,19 @@ export class WirePaymentsCreatorComponent {
this.submitted = true;
this.error = '';
if (await this.web3Wallet.isLocal()) {
const action = await this.web3Wallet.setupMetamask();
switch (action) {
case GetMetamaskComponent.ACTION_CREATE:
this.router.navigate(['/wallet']);
this.inProgress = false;
this.overlayModal.dismiss();
break;
case GetMetamaskComponent.ACTION_CANCEL:
return;
}
}
let { done } = await this.wireService.submitWire(this.wire);
if (done) {
......
import { and } from "@angular/router/src/utils/collection";
// TODO actually implement these mocks when necessary for testing
export let web3WalletServiceMock = new function () {
......@@ -6,6 +8,7 @@ export let web3WalletServiceMock = new function () {
this.onChainInterfaceLabel = 'Metamask';
this.unavailable = false;
this.locked = false;
this.isLocalWallet = false;
this.isUnavailable = jasmine.createSpy('isUnavailable').and.callFake(() => {
return this.unavailable;
......@@ -29,6 +32,10 @@ export let web3WalletServiceMock = new function () {
return this.balance;
});
this.isLocal = jasmine.createSpy('isLocal').and.callFake(async () => {
return this.isLocalWallet;
});
this.getOnChainInterfaceLabel = jasmine.createSpy('getOnChainInterfaceLabel').and.callFake(() => {
return this.onChainInterfaceLabel ? this.onChainInterfaceLabel: 'Metamask';
});
......