...
 
Commits (2)
......@@ -14973,6 +14973,11 @@
"integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==",
"dev": true
},
"qrcodejs2": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/qrcodejs2/-/qrcodejs2-0.0.2.tgz",
"integrity": "sha1-Rlr+Xjnxn6zsuTLBH3oYYQkUauE="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
......
......@@ -43,6 +43,7 @@
"material-design-lite": "~1.3.0",
"medium-editor": "^5.23.2",
"plotly.js": "^1.47.4",
"qrcodejs2": "0.0.2",
"rxjs": "~6.5.2",
"socket.io-client": "^2.2.0",
"textarea-caret": "^3.1.0",
......
......@@ -98,6 +98,18 @@ m-app {
width: 300px;
}
.m-page__goBack {
display: flex;
align-items: center;
font-size: 13px;
font-weight: bold;
text-decoration: none;
margin-bottom: 8px;
@include m-theme() {
color: themed($m-grey-600);
}
}
.m-page--sidebar--navigation--item {
cursor: pointer;
display: block;
......
......@@ -32,6 +32,7 @@ import {
import { Scheduler } from './components/scheduler/scheduler';
import { Modal } from './components/modal/modal.component';
import { MindsRichEmbed } from './components/rich-embed/rich-embed';
import { QRCodeComponent } from './components/qr-code/qr-code.component';
import { MDL_DIRECTIVES } from './directives/material';
import { AutoGrow } from './directives/autogrow';
......@@ -136,6 +137,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
MindsRichEmbed,
TagcloudComponent,
DropdownComponent,
QRCodeComponent,
AutoGrow,
InlineAutoGrow,
......@@ -220,6 +222,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
MindsRichEmbed,
TagcloudComponent,
DropdownComponent,
QRCodeComponent,
AutoGrow,
InlineAutoGrow,
......
......@@ -66,3 +66,40 @@ minds-button-thumbs-down {
}
}
}
.m-selector {
position: relative;
select {
padding: 8px 16px;
max-width: 100%;
appearance: none;
display: block;
width: 100%;
font-family: 'Roboto', Helvetica, sans-serif;
font-size: 13px;
cursor: pointer;
font-weight: 600;
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
}
&::before {
content: '\25bc';
position: absolute;
pointer-events: none;
top: 0;
bottom: 1px;
padding-top: 0.7em;
line-height: 1;
right: 0;
width: 2em;
text-align: center;
transform: scale(0.84, 0.42);
filter: progid:DXImageTransform.Microsoft.Matrix(M11=.84, M12=0, M21=0, M22=.42, SizingMethod='auto expand');
@include m-theme() {
color: themed($m-grey-500);
}
}
}
import { Component, Input, ElementRef } from '@angular/core';
declare var require: any;
let QRCode: any;
@Component({
selector: 'm-qr-code',
template: '',
})
export class QRCodeComponent {
qrcode;
@Input() data: string = '';
constructor(public el: ElementRef) {}
ngOnInit() {
if (!QRCode) {
QRCode = require('qrcodejs2');
}
this.qrcode = new QRCode(this.el.nativeElement, {
colorDark: '#000',
colorLight: '#FFF',
correctLevel: QRCode.CorrectLevel['M'],
height: 300,
text: this.data || ' ',
useSVG: true,
width: 300,
});
}
}
......@@ -71,24 +71,23 @@ minds-button-remind {
.minds-boost-button {
font-size: 12px;
font-weight: 400;
height: auto;
min-height: 0;
line-height: 18px;
text-transform: capitalize;
text-transform: uppercase;
align-self: center;
padding: 3px 0;
min-width: 62px;
min-width: 72px;
width: auto;
margin: -3px 0;
display: block;
@include m-theme() {
background-color: themed($m-blue) !important;
}
flex: 0 !important;
&:hover {
@include m-theme() {
background-color: rgba(themed($m-blue-dark), 0.9) !important;
}
span {
min-width: 72px;
text-align: center;
vertical-align: middle;
}
}
......
.tabs .m-wire-button {
transform: scale(0.8) translateY(-4px);
// transform: scale(0.8) translateY(-4px);
margin: -3px 0;
}
.m-pin-button {
overflow: visible;
......@@ -94,9 +95,9 @@ minds-activity {
padding-left: 8px;
margin: auto;
padding: 3px 0 3px 8px;
padding: 2px 0 2px 8px;
font-size: 14px;
line-height: 9px;
line-height: 8px;
border-radius: 3px;
vertical-align: middle;
display: flex;
......@@ -124,7 +125,7 @@ minds-activity {
}
.m-activity--metrics-metric {
font-size: 10px;
font-size: 9px;
display: inline-block;
vertical-align: middle;
......
......@@ -400,28 +400,28 @@
<minds-button-thumbs-up [object]="activity"></minds-button-thumbs-up>
<minds-button-thumbs-down [object]="activity"></minds-button-thumbs-down>
<m-wire-button
*ngIf="session.getLoggedInUser().guid != (activity.remind_object ? activity.remind_object.owner_guid : activity.owner_guid)"
[object]="activity.remind_object ? activity.remind_object : activity"
*ngIf="session.getLoggedInUser().guid != activity.owner_guid"
[object]="activity"
(done)="wireSubmitted($event)"
></m-wire-button>
<button
class="m-btn m-btn--action m-btn--slim minds-boost-button"
*ngIf="session.getLoggedInUser().guid == activity.owner_guid"
id="boost-actions"
(click)="showBoost()"
>
<span i18n="verb|@@M__ACTION__BOOST">Boost</span>
</button>
<minds-button-comment
[object]="activity"
(click)="openComments()"
></minds-button-comment>
<minds-button-remind [object]="activity"></minds-button-remind>
<a
class="mdl-button mdl-color-text--white mdl-button--colored minds-boost-button"
*ngIf="session.getLoggedInUser().guid == activity.owner_guid"
id="boost-actions"
(click)="showBoost()"
>
<ng-container i18n="verb|@@M__ACTION__BOOST">Boost</ng-container>
</a>
</div>
<!-- Activity metrics -->
<div
class="impressions-tag m-activity--metrics"
[class.m-activity--metrics-wire]="!session.getLoggedInUser() || session.getLoggedInUser().guid != activity.owner_guid"
class="impressions-tag m-activity--metrics m-activity--metrics-wire"
*ngIf="!activity.hide_impressions && !hideTabs"
>
<div class="m-activity--metrics-inner m-border">
......
<div class="m-page">
<div class="m-page--sidebar">
<m-wallet--balance-usd></m-wallet--balance-usd>
<m-wallet--balance-tokens></m-wallet--balance-tokens>
<div class="m-page--sidebar--navigation">
<a
class="m-page__goBack"
routerLink="/wallet/tokens"
style="margin-top: 8px"
>
<i class="material-icons">
keyboard_arrow_left
</i>
<span>Back to the Token Wallet</span>
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/wallet/usd/earnings"
......@@ -33,6 +44,12 @@
</div>
<div class="m-page--main">
<a class="m-page__goBack" routerLink="/wallet/tokens">
<i class="material-icons">
keyboard_arrow_left
</i>
<span>Back to the Token Wallet</span>
</a>
<router-outlet></router-outlet>
</div>
</div>
......@@ -4,6 +4,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ChartColumn } from '../../../common/components/chart/chart.component';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
@Component({
moduleId: module.id,
......@@ -15,6 +16,16 @@ export class RevenueConsoleComponent {
private client: Client,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
private router: Router
private router: Router,
public session: Session
) {}
ngOnInit() {
if (
!this.session.getLoggedInUser().merchant ||
this.session.getLoggedInUser().merchant.deleted
) {
this.router.navigate(['/wallet/usd/onboarding']);
}
}
}
......@@ -23,7 +23,7 @@
<div class="m-revenue--options-payout-method-bank-account">
<div class="m-revenue--options-payout-method-bank--name">
{{ payoutMethod.account.bank }}
{{ payoutMethod.account.bank_name }}
</div>
<div class="m-revenue--options-payout-method-bank--number">
****{{ payoutMethod.account.last4 }}
......
......@@ -40,29 +40,28 @@ export class RevenueOptionsComponent {
getSettings() {
this.inProgress = true;
this.client
.get('api/v1/monetization/settings')
.then(({ bank, country }) => {
this.inProgress = false;
this.payoutMethod.country = country;
this.form.controls.country.setValue(country);
if (bank.last4) {
this.payoutMethod.account = bank;
}
this.detectChanges();
});
this.client.get('api/v2/payments/stripe/connect').then(({ account }) => {
this.inProgress = false;
this.payoutMethod.country = account.country;
this.form.controls.country.setValue(account.country);
if (account.bankAccount.last4) {
this.payoutMethod.account = account.bankAccount;
}
this.detectChanges();
});
}
addBankAccount() {
this.inProgress = true;
this.error = '';
this.editing = false;
// this.editing = false;
this.detectChanges();
this.client
.post('api/v1/monetization/settings', this.form.value)
.post('api/v2/payments/stripe/connect/bank', this.form.value)
.then((response: any) => {
this.inProgress = false;
this.editing = false;
this.getSettings();
})
.catch(e => {
......@@ -76,7 +75,7 @@ export class RevenueOptionsComponent {
this.leaving = true;
this.detectChanges();
this.client
.delete('api/v1/monetization/settings/account')
.delete('api/v2/payments/stripe/connect')
.then((response: any) => {
(<any>window).Minds.user.merchant = [];
this.router.navigate(['/newsfeed']);
......
<div class="m-btc__wrapper">
<p>
Please scan the following QR code, or send <b>{{ amount }} BTC</b> to
<b>{{ address }}</b
>.
</p>
<m-qr-code [data]="qrdata"> </m-qr-code>
</div>
.m-btc__wrapper {
m-qr-code {
width: 300px;
height: 300px;
display: block;
position: relative;
}
}
import {
Component,
ChangeDetectorRef,
ChangeDetectionStrategy,
} from '@angular/core';
@Component({
selector: 'm-btc',
templateUrl: 'btc.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BTCComponent {
address: string = '1DWPuJjcZWzsRPCwss4gYqgeUpkj5AD1yu';
amount: string = '0.01';
set data(data) {
this.address = data.address;
this.amount = data.amount;
}
get qrdata() {
return 'bitcoin:' + this.address + '?amount=' + this.amount;
}
}
import { Injectable } from '@angular/core';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { BTCComponent } from './btc.component';
@Injectable()
export class BTCService {
constructor(private overlayModal: OverlayModalService) {}
showModal(opts) {
this.overlayModal.create(BTCComponent, opts).present();
}
}
<form class="m-form">
<p>
You can receive Bitcoin (BTC) payments via wire by inputing a receiver
address below. Note: You may want to rotate this address frequently to avoid
3rd parties tracking your transactions.
</p>
<label>Bitcoin Address</label>
<input
class="m-input"
[(ngModel)]="btcAddress"
[ngModelOptions]="{ standalone: true }"
/>
<button
class="m-btn m-btn--slim m-btn--action"
[disabled]="!btcAddress || saving"
(click)="save()"
>
<ng-container *ngIf="saving">
Saving...
</ng-container>
<ng-container *ngIf="!saving">
Save
</ng-container>
</button>
</form>
m-btc__settings {
p {
padding-right: $minds-padding * 2;
}
label {
font-weight: 600;
}
input {
margin: 8px 0;
border: 1px solid #ccc;
border-radius: 32px;
}
}
import {
Component,
ChangeDetectorRef,
ChangeDetectionStrategy,
} from '@angular/core';
import { Client } from '../../../services/api';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
@Component({
selector: 'm-btc__settings',
templateUrl: 'settings.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BTCSettingsComponent {
btcAddress: string = '';
saving: boolean = false;
constructor(
private client: Client,
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService
) {}
ngOnInit() {
this.getAddressFromRemote();
}
async getAddressFromRemote() {
const { address } = <any>await this.client.get('api/v2/wallet/btc/address');
this.btcAddress = address;
this.detectChanges();
}
async save() {
this.saving = true;
await this.client.post('api/v2/wallet/btc/address', {
address: this.btcAddress,
});
this.saving = false;
this.detectChanges();
this.overlayModal.dismiss();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<div
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
[hidden]="intentKey"
></div>
<iframe [src]="url" #iframe *ngIf="intentKey"> </iframe>
import {
Component,
EventEmitter,
Input,
Output,
ChangeDetectorRef,
ChangeDetectionStrategy,
ViewChild,
ElementRef,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Client } from '../../../services/api';
import { WalletService } from '../../../services/wallet';
import { Storage } from '../../../services/storage';
import { Session } from '../../../services/session';
@Component({
selector: 'm-payments__newCard',
templateUrl: 'new-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentsNewCard {
minds = (<any>window).Minds;
intentKey: string = '';
intentId: string = '';
@ViewChild('iframe', { static: false }) iframe: ElementRef;
@Output() completed: EventEmitter<void> = new EventEmitter();
_opts: any;
set opts(opts: any) {
this._opts = opts;
}
constructor(
public session: Session,
public client: Client,
public cd: ChangeDetectorRef,
private sanitizer: DomSanitizer
) {}
ngOnInit() {
window.addEventListener(
'message',
msg => {
if (msg.data === 'completed-saved-card') {
this.saveCard();
}
},
false
);
this.setupIntent();
}
async setupIntent() {
const { intent } = <any>(
await this.client.put('api/v2/payments/stripe/intents/setup')
);
this.intentKey = intent.client_secret;
this.intentId = intent.id;
this.detectChanges();
}
get url() {
const url =
'https://checkout.minds.com/stripe?intent_key=' + this.intentKey;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
async saveCard() {
const { success } = <any>await this.client.post(
'api/v2/payments/stripe/paymentmethods/apply',
{
intent_id: this.intentId,
}
);
this.intentKey = '';
this.completed.next();
this._opts.onCompleted();
this.detectChanges();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -8,10 +8,31 @@ import { ModalsModule } from '../modals/modals.module';
import { PayWall } from './paywall/paywall.component';
import { PaywallCancelButton } from './paywall/paywall-cancel.component';
import { PaymentsNewCard } from './new-card/new-card.component';
import { PaymentsSelectCard } from './select-card/select-card.component';
import { BTCService } from './btc/btc.service';
import { BTCComponent } from './btc/btc.component';
import { BTCSettingsComponent } from './btc/settings.component';
@NgModule({
imports: [NgCommonModule, CommonModule, CheckoutModule, ModalsModule],
declarations: [PayWall, PaywallCancelButton],
exports: [PayWall, PaywallCancelButton],
imports: [
NgCommonModule,
FormsModule,
ReactiveFormsModule,
CommonModule,
CheckoutModule,
ModalsModule,
],
declarations: [
PayWall,
PaywallCancelButton,
PaymentsNewCard,
PaymentsSelectCard,
BTCComponent,
BTCSettingsComponent,
],
exports: [PayWall, PaywallCancelButton, PaymentsNewCard, PaymentsSelectCard],
providers: [BTCService],
entryComponents: [PaymentsNewCard, BTCComponent, BTCSettingsComponent],
})
export class PaymentsModule {}
<div class="m-selector">
<select
[ngModel]="paymentMethodId"
(ngModelChange)="paymentMethodId = $event; selected.next(paymentMethodId)"
>
<option *ngFor="let card of paymentMethods" [value]="card.id">
{{ card.card_brand | uppercase }} *** {{ card.card_last4 }}
{{ card.card_expires }}
</option>
<option value="new">Add a new card</option>
</select>
</div>
<div class="m-paymentsSelectCard__addNewCard" *ngIf="paymentMethodId === 'new'">
<m-payments__newCard (completed)="loadCards()"> </m-payments__newCard>
</div>
.m-paymentsSelectCard__addNewCard {
margin-top: 8px;
@include m-theme() {
border-top: 2px solid themed($m-grey-100);
}
iframe {
max-height: 112px;
}
}
import {
Component,
EventEmitter,
Input,
Output,
ChangeDetectorRef,
ChangeDetectionStrategy,
ViewChild,
ElementRef,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { Client } from '../../../services/api';
import { WalletService } from '../../../services/wallet';
import { Storage } from '../../../services/storage';
import { Session } from '../../../services/session';
@Component({
selector: 'm-payments__selectCard',
templateUrl: 'select-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentsSelectCard {
minds = (<any>window).Minds;
@Output() selected: EventEmitter<string> = new EventEmitter();
paymentMethodId: string = '';
paymentMethods = [];
constructor(
public session: Session,
public client: Client,
public cd: ChangeDetectorRef,
private sanitizer: DomSanitizer
) {}
ngOnInit() {
this.loadCards();
}
async loadCards() {
const { paymentmethods } = <any>(
await this.client.get('api/v2/payments/stripe/paymentmethods')
);
this.paymentMethods = paymentmethods;
this.paymentMethodId = paymentmethods[0].id;
this.selected.next(this.paymentMethodId);
this.detectChanges();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
<div class="m-settings--section m-border" *ngIf="cards.length">
<div class="m-settings--section m-border">
<h4 i18n="@@SETTINGS__BILLING__SAVED_CARDS__TITLE">Payment Methods</h4>
<div [hidden]="!inProgress" style="width:100%; text-align:center;">
......@@ -16,14 +16,14 @@
class="m-settings--billing-saved-cards--cards-item"
>
<span class="m-settings--billing-saved-cards--cards-item-type">{{
card.brand
card.card_brand
}}</span>
<span class="m-settings--billing-saved-cards--cards-item-number"
>**** {{ card.last4 }}</span
>
<span class="m-settings--billing-saved-cards--cards-item-expiry"
>{{ card.exp_month }} / {{ card.exp_year }}</span
>**** {{ card.card_last4 }}</span
>
<span class="m-settings--billing-saved-cards--cards-item-expiry">{{
card.expires
}}</span>
<span
class="m-settings--billing-saved-cards--cards-item-select"
(click)="removeCard(i)"
......@@ -31,6 +31,25 @@
>Remove</span
>
</li>
<li
class="m-settings--billing-saved-cards--cards-item m-settings--billing-saved-cards--cards-item-new"
>
<span
class="m-settings--billing-saved-cards--cards-item-type"
i18n="@@SETTINGS__BILLING__SAVED_CARDS__ADD_NEW_CARD_LABEL"
>Add a new card</span
>
<span
class="m-settings--billing-saved-cards--cards-item-select"
(click)="addNewCard()"
i18n="@@SETTINGS__BILLING__SAVED_CARDS__ADD_ACTION"
>ADD</span
>
</li>
</ul>
</div>
<m-payments__newCard (completed)="loadSaveCards()" *ngIf="addingNewCard">
</m-payments__newCard>
</div>
import { Component, ChangeDetectorRef } from '@angular/core';
import { Client } from '../../../../common/api/client.service';
import { OverlayModalService } from '../../../../services/ux/overlay-modal';
import { PaymentsNewCard } from '../../../payments/new-card/new-card.component';
@Component({
selector: 'm-settings--billing-saved-cards',
......@@ -9,23 +11,17 @@ import { Client } from '../../../../common/api/client.service';
export class SettingsBillingSavedCardsComponent {
minds = window.Minds;
inProgress: boolean = false;
addNewCard: boolean = false;
addingNewCard: boolean = false;
cards: Array<any> = [];
constructor(private client: Client, private cd: ChangeDetectorRef) {}
constructor(
private client: Client,
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService
) {}
ngOnInit() {
this.loadSavedCards();
this.setupStripe();
setTimeout(() => {
this.setupStripe();
}, 1000); //sometimes stripe can take a while to download
}
setupStripe() {
if ((<any>window).Stripe) {
(<any>window).Stripe.setPublishableKey(this.minds.stripe_key);
}
}
loadSavedCards(): Promise<any> {
......@@ -33,12 +29,12 @@ export class SettingsBillingSavedCardsComponent {
this.cards = [];
return this.client
.get(`api/v1/payments/stripe/cards`)
.then(({ cards }) => {
.get(`api/v2/payments/stripe/paymentmethods`)
.then(({ paymentmethods }) => {
this.inProgress = false;
if (cards && cards.length) {
this.cards = cards;
if (paymentmethods && paymentmethods.length) {
this.cards = paymentmethods;
this.detectChanges();
}
})
......@@ -52,7 +48,7 @@ export class SettingsBillingSavedCardsComponent {
this.inProgress = true;
this.client
.delete('api/v1/payments/stripe/card/' + this.cards[index].id)
.delete('api/v2/payments/stripe/paymentmethods/' + this.cards[index].id)
.then(() => {
this.cards.splice(index, 1);
......@@ -65,55 +61,20 @@ export class SettingsBillingSavedCardsComponent {
});
}
setCard(card) {
this.inProgress = true;
this.detectChanges();
this.getCardNonce(card)
.then(token => {
this.saveCard(token)
.then(() => {
this.inProgress = false;
this.addNewCard = false;
this.detectChanges();
this.loadSavedCards();
})
.catch(e => {
this.inProgress = false;
this.detectChanges();
alert((e && e.message) || 'There was an error saving your card.');
});
})
.catch(e => {
this.inProgress = false;
this.detectChanges();
alert(
(e && e.message) || 'There was an error with your card information.'
);
});
}
saveCard(token: string): Promise<any> {
return this.client.put('api/v1/payments/stripe/card/' + token);
}
getCardNonce(card): Promise<string> {
return new Promise((resolve, reject) => {
(<any>window).Stripe.card.createToken(
addNewCard() {
this.overlayModal
.create(
PaymentsNewCard,
{},
{
number: card.number,
cvc: card.sec,
exp_month: card.month,
exp_year: card.year,
},
(status, response) => {
if (response.error) {
return reject(response.error.message);
}
return resolve(response.id);
class: '',
onCompleted: () => {
this.loadSavedCards(); //refresh list
this.overlayModal.dismiss();
},
}
);
});
)
.present();
}
detectChanges(): void {
......
......@@ -61,6 +61,9 @@
i18n="@@SETTINGS__BILLING__SUBSCRIPTIONS__POINTS_LABEL"
>{{ subscription.amount | number: '1.0-0' }} Points</ng-template
>
<ng-template ngSwitchCase="usd"
>{{ subscription.amount | token: 2 }} USD</ng-template
>
</span>
<span
......
......@@ -57,11 +57,20 @@
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/wallet/tokens/testnet"
routerLink="/wallet/usd"
routerLinkActive="m-page--sidebar--navigation--item-active"
*mIfFeature="'wire-multi-currency'"
>
<i class="material-icons">link</i>
<span i18n="@@WALLET__TOKENS__TESTNET_TOKENS">Testnet Tokens</span>
<i class="material-icons">attach_money</i>
<span i18n="@@WALLET__TOKENS__TESTNET_TOKENS">USD Console</span>
</a>
<a
class="m-page--sidebar--navigation--item"
(click)="openBtcSettingsModal()"
*mIfFeature="'wire-multi-currency'"
>
<i class="material-icons">settings</i>
<span i18n="@@WALLET__TOKENS__TESTNET_TOKENS">BTC Console</span>
</a>
<a
class="m-page--sidebar--navigation--item"
......
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { BTCSettingsComponent } from '../../payments/btc/settings.component';
@Component({
moduleId: module.id,
selector: 'm-wallet--tokens',
templateUrl: 'tokens.component.html',
})
......@@ -10,11 +12,18 @@ export class WalletTokensComponent {
showOnboarding: boolean = false;
minds = window.Minds;
constructor(route: ActivatedRoute) {
constructor(
route: ActivatedRoute,
private overlayModal: OverlayModalService
) {
route.url.subscribe(() => {
this.showOnboarding =
route.snapshot.firstChild &&
route.snapshot.firstChild.routeConfig.path === 'transactions';
});
}
openBtcSettingsModal() {
this.overlayModal.create(BTCSettingsComponent, {}).present();
}
}
<form
(submit)="submit()"
[formGroup]="form"
#f="ngForm"
class="m-form m-walletUsd__onboarding mdl-color--white m-border"
>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col">
<label i18n="@@M__COMMON__COUNTRY">Country</label>
<minds-country-input
[allowed]="[
'AT',
'AU',
'BE',
'CA',
'CH',
'DE',
'DK',
'ES',
'FI',
'FR',
'GB',
'HK',
'IE',
'IT',
'LU',
'NL',
'NO',
'NZ',
'PT',
'SE',
'SG',
'US'
]"
[country]="form.controls.country.value"
(countryChange)="form.controls.country.setValue($event)"
></minds-country-input>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="!isCountry(['JP'])">
<label i18n="@@M__COMMON__FIRST_NAME">First name</label>
<input
formControlName="firstName"
type="text"
placeholder="First name"
i18n-placeholder="@@M__COMMON__FIRST_NAME"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="!isCountry(['JP'])">
<label i18n="@@M__COMMON__LAST_NAME">Last name</label>
<input
formControlName="lastName"
type="text"
placeholder="Last name"
i18n-placeholder="@@M__COMMON__LAST_NAME"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col">
<label i18n="@@MONETIZATION__ONBOARDING__DOB_LABEL">Date of Birth</label>
<minds-date-input
[date]="form.controls.dob.value"
(dateChange)="form.controls.dob.setValue($event)"
[disabled]="restrictAsVerified"
></minds-date-input>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['JP'])">
<label i18n="@@MONETIZATION__ONBOARDING__GENDER_LABEL">Gender</label>
<select formControlName="gender">
<option value=""></option>
<option value="female" i18n="@@MONETIZATION__ONBOARDING__FEMALE_OPTION"
>Female</option
>
<option value="male" i18n="@@MONETIZATION__ONBOARDING__MALE_OPTION"
>Male</option
>
</select>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['US'])">
<label i18n="@@MONETIZATION__ONBOARDING__SSN_LAST_4_LABEL"
>SSN (last 4 digits)</label
>
<input
formControlName="ssn"
type="text"
placeholder="eg. 3333"
i18n="@@MONETIZATION__ONBOARDING__SSN_LAST_4_PLACEHOLDER"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['CA', 'HK', 'SG'])">
<label>
<ng-container i18n="@@MONETIZATION__ONBOARDING__PERSONAL_ID_LABEL"
>Personal ID</ng-container
>
<m-tooltip
*ngIf="isCountry(['CA'])"
icon="help"
style="vertical-align: middle;"
i18n="@@MONETIZATION__ONBOARDING__CA_PERSONAL_ID_TOOLTIP"
>
Social Insurance Number (SIN).
</m-tooltip>
<m-tooltip
*ngIf="isCountry(['HK'])"
icon="help"
style="vertical-align: middle;"
i18n="@@MONETIZATION__ONBOARDING__HK_PERSONAL_ID_TOOLTIP"
>
Hong Kong Identity Card (HKID).
</m-tooltip>
<m-tooltip
*ngIf="isCountry(['SG'])"
icon="help"
style="vertical-align: middle;"
i18n="@@MONETIZATION__ONBOARDING__SG_PERSONAL_ID_TOOLTIP"
>
National Registration Identity Card (NRIC).
</m-tooltip>
</label>
<input
formControlName="personalIdNumber"
type="text"
placeholder="Personal ID number or code"
i18n-placeholder="@@MONETIZATION__ONBOARDING__PERSONAL_ID_PLACEHOLDER"
class="m-input"
/>
</div>
</div>
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--6-col" *ngIf="!isCountry(['JP'])">
<label i18n="@@MONETIZATION__ONBOARDING__ADDRESS_LABEL">Address</label>
<input
formControlName="street"
type="text"
placeholder="House name/number, Street name"
i18n-placeholder="@@MONETIZATION__ONBOARDING__ADDRESS_PLACEHOLDER"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="!isCountry(['JP', 'SG'])">
<label i18n="@@MONETIZATION__ONBOARDING__CITY_LABEL">City</label>
<input
formControlName="city"
type="text"
placeholder="eg. New York City"
i18n-placeholder="@@MONETIZATION__ONBOARDING__CITY_PLACEHOLDER"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['US'])">
<label i18n="State as a country entity@@M__COMMON__COUNTRY_STATE"
>State</label
>
<minds-state-input
[state]="form.controls.state.value"
(stateChange)="form.controls.state.setValue($event)"
[disabled]="restrictAsVerified"
></minds-state-input>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['AU', 'CA', 'IE'])">
<label i18n="@@MONETIZATION__ONBOARDING__STATE_PROVINCE_LABEL"
>State / Province</label
>
<input
formControlName="state"
type="text"
placeholder="State or province"
i18n-placeholder="@@MONETIZATION__ONBOARDING__STATE_PROVINCE_PLACEHOLDER"
class="m-input"
/>
</div>
<div
class="mdl-cell mdl-cell--6-col"
*ngIf="!isCountry(['HK', 'IE', 'JP'])"
>
<label i18n="@@MONETIZATION__ONBOARDING__ZIP_CODE_LABEL"
>Zip (Postal) Code</label
>
<input
formControlName="postCode"
type="text"
placeholder="eg. 10001"
i18n-placeholder="@@MONETIZATION__ONBOARDING__ZIP_CODE_PLACEHOLDER"
class="m-input"
/>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="isCountry(['JP'])">
<label i18n="@@MONETIZATION__ONBOARDING__PHONE_LABEL">Phone Number</label>
<input
formControlName="phoneNumber"
type="tel"
placeholder="eg. 123-456789"
i18n-placeholder="@@MONETIZATION__ONBOARDING__PHONE_PLACEHOLDER"
class="m-input"
/>
</div>
</div>
<div class="m-merchant-legal mdl-color--grey-50 mdl-color-text--grey-600">
<label>
<input formControlName="stripeAgree" type="checkbox" value="1" />
<ng-container i18n="@@MONETIZATION__ONBOARDING__AGREE_TERMS_LABEL"
>I have read and agree to the
<a (click)="showTerms()">Monetization Terms & Conditions</a
>.</ng-container
>
</label>
</div>
<div *ngIf="error" class="m-error mdl-color-text--red">
{{ error }}
</div>
<div class="m-merchant-form-action">
<button
type="submit"
class="m-btn m-btn--slim m-btn--action"
[disabled]="inProgress || !f.form.valid"
>
<span i18n="@@MONETIZATION__ONBOARDING__NEXT_ACTION">Next</span>
</button>
</div>
</form>
.m-walletUsd__onboarding {
font-family: 'Roboto';
label {
display: block;
text-transform: uppercase;
letter-spacing: 1px;
margin: 8px 0;
font-weight: 600;
}
minds-country-input {
width: 100%;
}
select {
padding: 12px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
font-family: 'Roboto';
font-weight: 300;
cursor: pointer;
-webkit-appearance: none;
@include m-theme() {
color: themed($m-grey-500);
background: themed($m-white);
border-color: themed($m-grey-100);
}
}
.m-date-input--field select {
padding: 12px 40px 12px 12px;
text-transform: uppercase;
@include m-theme() {
color: themed($m-grey-500);
}
margin-right: 4px;
}
input[type='text'] {
padding: 12px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
border-radius: 6px;
width: 100%;
}
}
.m-merchant-legal {
p {
margin: 0;
}
a {
cursor: pointer;
}
}
.m-merchant-form-action {
margin: 16px;
}
import {
Component,
OnInit,
Output,
EventEmitter,
Input,
ChangeDetectorRef,
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Client } from '../../../../services/api';
import { requiredFor, optionalFor } from './onboarding.validators';
import { OverlayModalService } from '../../../../services/ux/overlay-modal';
import { WalletUSDTermsComponent } from '../terms.component';
@Component({
selector: 'm-walletUsd__onboarding',
templateUrl: 'onboarding.component.html',
})
export class WalletUSDOnboardingComponent implements OnInit {
form: FormGroup;
inProgress: boolean = false;
restrictAsVerified: boolean = false;
minds = window.Minds;
merchant: any;
error: string;
@Input() edit: boolean = false;
@Output() completed: EventEmitter<any> = new EventEmitter<any>();
constructor(
private fb: FormBuilder,
private client: Client,
private cd: ChangeDetectorRef,
private router: Router,
protected overlayModal: OverlayModalService
) {}
ngOnInit() {
this.form = this.fb.group({
country: ['', Validators.required],
ssn: ['', requiredFor(['US'], { ignore: this.edit })],
personalIdNumber: [
'',
requiredFor(['CA', 'HK', 'SG'], { ignore: this.edit }),
],
firstName: ['', optionalFor(['JP'])],
lastName: ['', optionalFor(['JP'])],
gender: ['', requiredFor(['JP'])],
dob: ['', Validators.required],
street: ['', optionalFor(['JP'])],
city: ['', optionalFor(['JP', 'SG'])],
state: ['', requiredFor(['AU', 'CA', 'IE', 'US'])],
postCode: ['', optionalFor(['HK', 'IE', 'JP'])],
phoneNumber: ['', requiredFor(['JP'])],
stripeAgree: ['', Validators.required],
});
this.restrictAsVerified = false;
if (this.merchant) {
if (this.edit) {
this.merchant.stripeAgree = true;
this.restrictAsVerified = this.merchant.verified;
}
this.form.patchValue(this.merchant);
}
this.disableRestrictedFields();
}
@Input('merchant') set _merchant(value) {
if (!value) {
return;
}
this.restrictAsVerified = false;
if (this.form) {
if (this.edit) {
value.stripeAgree = true;
}
this.form.patchValue(value);
}
this.merchant = value;
this.restrictAsVerified = this.merchant.verified;
this.disableRestrictedFields();
}
submit() {
if (!this.edit) {
this.onboard();
} else {
this.update();
}
}
async onboard() {
if (this.inProgress) {
return;
}
this.inProgress = true;
this.error = '';
try {
const response = <any>(
await this.client.put('api/v2/wallet/usd/account', this.form.value)
);
this.inProgress = false;
if (!this.minds.user.programs) this.minds.user.programs = [];
this.minds.user.programs.push('affiliate');
this.minds.user.merchant = {
id: response.account.id,
service: 'stripe',
};
this.router.navigate(['/wallet/usd/']);
} catch (e) {
this.inProgress = false;
this.error = e.message;
this.detectChanges();
}
}
update() {
if (this.inProgress) {
return;
}
this.inProgress = true;
this.error = '';
this.client
.post('api/v2/wallet/usd/account', this.form.value)
.then((response: any) => {
this.inProgress = false;
this.completed.emit(response);
this.detectChanges();
})
.catch(e => {
this.inProgress = false;
this.error = e.message;
this.detectChanges();
});
}
disableRestrictedFields() {
if (!this.form) {
return;
}
const action = this.restrictAsVerified ? 'disable' : 'enable';
this.form.controls.firstName[action]();
this.form.controls.lastName[action]();
this.form.controls.gender[action]();
this.form.controls.dob[action]();
this.form.controls.street[action]();
this.form.controls.city[action]();
this.form.controls.state[action]();
this.form.controls.postCode[action]();
this.form.controls.phoneNumber[action]();
}
isCountry(countries: string[]) {
const currentCountry = this.form.controls.country.value;
return countries.indexOf(currentCountry) > -1;
}
showTerms() {
this.overlayModal.create(WalletUSDTermsComponent).present();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
import { ValidatorFn, AbstractControl } from '@angular/forms';
const _isCountry = (currentCountry, countries: string[]) => {
return countries.indexOf(currentCountry) > -1;
};
export function requiredFor(
countryCodes: string[],
{ ignore = false }: { ignore?: boolean } = {}
): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
if (ignore) {
return null;
}
const country = control.root.get('country');
if (!country) {
return { required: true };
}
const selected = country.value;
if (!_isCountry(selected, countryCodes)) {
return null;
}
return !control.value ? { required: true } : null;
};
}
export function optionalFor(
countryCodes: string[],
{ ignore = false }: { ignore?: boolean } = {}
): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } => {
if (ignore) {
return null;
}
const country = control.root.get('country');
if (!country) {
return { required: true };
}
const selected = country.value;
if (_isCountry(selected, countryCodes)) {
return null;
}
return !control.value ? { required: true } : null;
};
}
<div class="m-walletUsd__terms">
<h3 i18n="@@MONETIZATION__TERMS__TITLE">Terms & Conditions</h3>
<p style="font-weight:bold" i18n>
If you have been invited to participate in and have opted into our Minds
Monetization Program (the "Program"), and your participation in The Program
has not been terminated, then the following terms shall apply.
</p>
<p i18n>
By participating in The Program you agree to become bound by the terms and
conditions herein (“The Agreement”). If you do not agree to all of the terms
and conditions herein you may not participate in The Program and are not
eligible to receive The Program services (including, but not limited to,
Program payments). To the extent that these terms and conditions are
considered an offer on behalf of Minds, acceptance of such offer is
expressly limited to these terms.
</p>
<p i18n>
You will comply with all of the terms and conditions contained in the Minds
Terms of Service and all other operating rules, policies, and procedures
that may be published by Minds.
</p>
<p i18n>
You will disclose to Minds any paid products or services you employ to refer
paid hosting customers to Minds.
</p>
<p i18n>
You are not authorized to act as an agent of Minds and shall not so
represent. You are not and shall not hold yourself out to be an employee of
Minds.
</p>
<p i18n>
You may not factually misrepresent Minds, Minds.com or The Program
including, but not limited to, the services, products, and terms and
conditions therefor.
</p>
<p i18n>
You hereby consent to the use of your name, likeness, blog name, and any
associated content or logos by Minds in connection with advertisements,
articles, and other similar communications conducted by Minds relating to
The Program.
</p>
<p i18n>
You must be at least 18 years old to participate in the the Program. By
agreeing to these terms, you represent that you are 18 or older.
</p>
<p i18n>
Program participants residing in the US must provide a tax ID if required.
</p>
<p i18n>
Subject to these terms of service, and provided that you are currently
participating in the Program:
</p>
<p i18n>
Minds will pay you 25% of the revenue share of the payments received by
Minds by customers who sign up through your Affiliate referral link. You are
not entitled to revenue sharing payments from Minds customers who fail to
register through your Affiliate referral link. Minds will issue payments
approximately 30 days following the end of each month in which the
applicable payment(s) are received by Minds.
</p>
<p i18n>
The following Minds products qualify the Affiliate to receive 25% of the
revenue Minds generates from users who sign up through the Affiliate’s
referral link:
<li>Point Purchases</li>
</p>
<p i18n>
All payments will be issued through a transaction medium determined by Minds
in its sole discretion. You must conform with Minds chosen payment method(s)
in order to be entitled to payment hereunder. Minds shall have no obligation
to make any payment under this paragraph until the outstanding amount owed
to you exceeds one hundred US dollars ($100). Your rights to Program
payments that remain unclaimed or undeliverable for a period of one year or
more may, in Minds sole discretion, be forfeit.
</p>
<p i18n>
If you wish to discontinue your participation in The Program, you may do so
in the Minds.com settings page of your participating account. Minds will
distribute remaining earnings to discontinued Program participants only if
the amount exceeds twenty five US dollars ($25).
</p>
<p i18n>
By agreeing to The Program terms, you acknowledge the possibility of
chargebacks due to fraudulent payments and refunds. In the event of a
chargeback or refund, Minds holds the right to retract any payout related to
the disputed transaction.
</p>
<p i18n>
Minds, in its sole discretion, may terminate your participation in The
Program at any time, with or without cause, with or without notice,
effective immediately.
</p>
<p i18n>
These terms of service are subject to change by Minds. Minds will take steps
to notify you of changes via the Minds.com website or email. You are
responsible for maintaining awareness and compliance with all future changes
to these terms and conditions. If you do not agree to be bound by revised
terms you must discontinue your participation in The Program immediately.
</p>
<p i18n>
The Minds services are provided “as is”. Minds and its suppliers and
licensors hereby disclaim all warranties of any kind, express or implied,
including, without limitation, the warranties of merchantability, fitness
for a particular purpose and non-infringement. Neither Minds nor its
suppliers and licensors, makes any warranty that its services will be error
free.
</p>
<p i18n>
You represent and warrant that your participation in The Program will be in
strict accordance with all applicable laws and regulations.
</p>
<p i18n>
You agree to indemnify and hold harmless Minds, its contractors, and its
licensor, and their respective directors, officers, employees and agents
from and against any and all claims and expenses, including attorneys’ fees,
arising out of your participation in The Program including but not limited
to your violation of this Agreement.
</p>
<p i18n>
If you choose to monetize your channel, your channel must respect and abide
by the the terms of Stripe for all services and Google Adsense if you are
leveraging the Ad-Revenue sharing feature. Failure to comply with these
terms of service may result in loss or ban of monetization privileges.
</p>
<p style="font-style: italic" i18n>
These Terms and Conditions constitute the entire agreement between Minds and
you concerning the subject matter hereof, and they may only be modified by a
written amendment signed by an authorized executive of Minds, or by the
posting by Minds of a revised version. Except to the extent applicable law,
if any, provides otherwise, these Terms and Conditions and/or your
participation in The Program shall be governed by the laws of the state of
Connecticut, U.S.A., excluding its conflict of law provisions, and the
proper venue for any disputes arising out of or relating to any of the same
will be the state and federal courts. Except for claims for injunctive or
equitable relief or claims regarding intellectual property rights (which may
be brought in any competent court without the posting of a bond), any
dispute arising under this Agreement shall be finally settled in accordance
with the Comprehensive Arbitration Rules of the Judicial Arbitration and
Mediation Service, Inc. (“JAMS”) by three arbitrators appointed in
accordance with such Rules. The arbitration shall take place in Hartford,
Connecticut, in the English language and the arbitral decision may be
enforced in any court. The prevailing party in any action or proceeding to
enforce this Agreement shall be entitled to costs and attorneys’ fees. If
any part of this Agreement is held invalid or unenforceable, that part will
be construed to reflect the parties’ original intent, and the remaining
portions will remain in full force and effect. A waiver by either party of
any term or condition of this Agreement or any breach thereof, in any one
instance, will not waive such term or condition or any subsequent breach
thereof. You may assign your rights under this Agreement to any party that
consents to, and agrees to be bound by, its terms and conditions; Minds may
assign its rights under this Agreement without condition. This Agreement
will be binding upon and will inure to the benefit of the parties, their
successors and permitted assigns.
</p>
</div>
.m-affiliate--terms {
font-family: 'Roboto';
h3 {
margin: 8px 0;
text-transform: uppercase;
font-size: 18px;
letter-spacing: 2px;
}
}
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
@Component({
selector: 'm-walletUsd__terms',
templateUrl: 'terms.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WalletUSDTermsComponent {}
......@@ -35,6 +35,8 @@ import { WalletUSDComponent } from './usd/usd.component';
import { WalletUSDEarningsComponent } from './usd/earnings.component';
import { WalletUSDPayoutsComponent } from './usd/payouts.component';
import { WalletUSDSettingsComponent } from './usd/settings.component';
import { WalletUSDOnboardingComponent } from './usd/onboarding/onboarding.component';
import { WalletUSDTermsComponent } from './usd/terms.component';
import { WalletTokenWithdrawLedgerComponent } from './tokens/withdraw/ledger/ledger.component';
import { WalletTokenAddressesComponent } from './tokens/addresses/addresses.component';
import { TokenOnboardingModule } from './tokens/onboarding/onboarding.module';
......@@ -87,6 +89,7 @@ const walletRoutes: Routes = [
{ path: 'earnings', component: WalletUSDEarningsComponent },
{ path: 'payouts', component: WalletUSDPayoutsComponent },
{ path: 'settings', component: WalletUSDSettingsComponent },
{ path: 'onboarding', component: WalletUSDOnboardingComponent },
],
},
{ path: 'wire', component: WalletWireComponent },
......@@ -138,6 +141,8 @@ const walletRoutes: Routes = [
WalletUSDEarningsComponent,
WalletUSDPayoutsComponent,
WalletUSDSettingsComponent,
WalletUSDOnboardingComponent,
WalletUSDTermsComponent,
WalletTokenAddressesComponent,
WalletTokenContributionsOverviewComponent,
WalletTokenContributionsChartComponent,
......@@ -154,7 +159,8 @@ const walletRoutes: Routes = [
WalletToggleComponent,
WalletFlyoutComponent,
WalletBalanceUSDComponent,
WalletBalanceTokensComponent,
],
entryComponents: [WalletComponent],
entryComponents: [WalletComponent, WalletUSDTermsComponent],
})
export class WalletModule {}
.m-wire-button {
appearance: none;
/*appearance: none;
background: transparent;
border: 0;
border-radius: 0;
padding: 0;
*/
padding: 3px;
height: auto;
line-height: 18px;
cursor: pointer;
@include m-theme() {
color: themed($m-blue);
......@@ -16,7 +19,13 @@
}
> .ion-icon {
transform: scale(1.6);
// transform: scale(1.6);
font-size: 18px;
}
span {
margin-left: $minds-margin;
vertical-align: middle;
}
&:focus {
......
......@@ -8,8 +8,12 @@ import { Session } from '../../../services/session';
@Component({
selector: 'm-wire-button',
template: `
<button class="m-wire-button" (click)="wire()">
<button
class="m-btn m-btn--action m-btn--slim m-wire-button"
(click)="wire()"
>
<i class="ion-icon ion-flash"></i>
<span>Wire</span>
</button>
`,
})
......
......@@ -87,6 +87,7 @@
font-weight: 400;
margin-bottom: 0;
padding-left: $minds-margin * 2;
padding-right: $minds-padding * 3;
line-height: 16px;
}
}
......@@ -208,7 +209,12 @@
display: inline-block;
}
> i {
font-size: 42px;
font-size: 52px;
display: block;
margin-left: -12px;
}
> img {
height: 52px;
display: block;
}
}
......@@ -395,16 +401,16 @@
appearance: none;
padding: 8px 32px;
background: none;
border-radius: 0;
//border-radius: 0;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
text-transform: uppercase;
cursor: pointer;
@include m-theme() {
border: 1px solid themed($m-grey-700);
color: themed($m-grey-700);
}
//@include m-theme(){
// border: 1px solid themed($m-grey-700);
// color: themed($m-grey-700);
//}
&[disabled] {
cursor: default;
......@@ -657,6 +663,7 @@
&:not(.m-wire--creator-selector--highlight) .m-wire--creator-selector-type {
> i,
h5 > span,
img,
.m-boost--creator-selector--hoverable,
.material-icons {
opacity: 0.5;
......@@ -834,24 +841,12 @@
// Creator Rewards component
.m-wire--creator-rewards {
display: block;
//border-left: 1px solid rgba(0, 0, 0, 0.2);
//padding: ($minds-padding * 3) 0 ($minds-padding * 3) 60px;
font-family: inherit;
font-weight: 200;
.m-wire--creator-rewards,
.m-wireCreator__rewardSelector {
@include m-theme() {
color: themed($m-grey-700);
}
@media screen and (max-width: $max-mobile) {
position: initial;
max-width: none;
border: none;
padding: 0 ($minds-padding * 2);
margin: ($minds-padding * 2) 0;
}
.m-wire--creator-rewards--title {
display: flex;
flex-direction: row;
......@@ -873,11 +868,8 @@
}
}
.m-wire--creator-rewards--list {
m-wirecreator__rewards {
//max-width: 360px;
display: flex;
flex-direction: row;
list-style: none;
margin: 0;
padding: 0;
@include m-theme() {
......@@ -909,13 +901,19 @@
letter-spacing: 2.5px;
}
.m-wire--creator-rewards--description p {
font-family: 'Roboto', Helvetica, sans-serif;
padding: 0;
margin: 4px 0;
line-height: 1.1;
font-size: 12px;
font-weight: 400;
.m-wire--creator-rewards--description {
padding: $minds-padding;
> * {
display: inline;
}
p {
font-family: 'Roboto', Helvetica, sans-serif;
padding: 0;
margin: 4px 0;
line-height: 1.1;
font-size: 12px;
font-weight: 400;
}
}
}
}
......@@ -970,3 +968,8 @@
}
}
}
.m-wireCreator__rewardSelector {
margin-right: 64px;
margin-bottom: 16px;
}
......@@ -47,6 +47,7 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { web3WalletServiceMock } from '../../../../tests/web3-wallet-service-mock.spec';
import { IfFeatureDirective } from '../../../common/directives/if-feature.directive';
import { FeaturesService } from '../../../services/features.service';
import { MockComponent } from '../../../utils/mock';
/* tslint:disable */
@Component({
......@@ -173,7 +174,7 @@ describe('WireCreatorComponent', () => {
function getAmountLabel(): DebugElement {
return fixture.debugElement.query(
By.css('span.m-wire--creator-wide-input--label')
By.css('.m-wire--creator-wide-input--label')
);
}
......@@ -202,6 +203,14 @@ describe('WireCreatorComponent', () => {
AddressExcerptPipe,
TokenPipe,
IfFeatureDirective,
MockComponent({
selector: 'm-wireCreator__rewards',
inputs: ['rewards', 'amount', 'currency', 'channel', 'sums'],
outputs: ['selectReward'],
}),
MockComponent({
selector: 'm-payments__selectCard',
}),
], // declare the test component
imports: [FormsModule, RouterTestingModule],
providers: [
......@@ -319,14 +328,6 @@ describe('WireCreatorComponent', () => {
jasmine.clock().uninstall();
});
it('should have a title', () => {
const title = fixture.debugElement.query(
By.css('.m-wire--creator--header span')
);
expect(title).not.toBeNull();
expect(title.nativeElement.textContent).toContain('Wire');
});
it("should have the target user's avatar", () => {
const avatar = fixture.debugElement.query(
By.css('.m-wire--creator--header-text .m-wire--avatar')
......@@ -351,9 +352,7 @@ describe('WireCreatorComponent', () => {
expect(subtitle).not.toBeNull();
expect(subtitle.nativeElement.textContent).toContain(
'Support @' +
comp.owner.username +
" by sending them tokens. Once you send them the amount listed in the tiers, you can receive rewards if they are offered. Otherwise, it's a donation."
'Support @' + comp.owner.username + ' by'
);
});
......@@ -374,7 +373,7 @@ describe('WireCreatorComponent', () => {
expect(title.nativeElement.textContent).toContain('Payment Method');
});
it('should have payment method list (onchain, offchain)', () => {
it('should have payment method list (tokens, eth, usd, btc)', () => {
const list = fixture.debugElement.query(
By.css(
'section.m-wire--creator-payment-section > ul.m-wire--creator-selector'
......@@ -382,7 +381,7 @@ describe('WireCreatorComponent', () => {
);
expect(list).not.toBeNull();
expect(list.nativeElement.children.length).toBe(3);
expect(list.nativeElement.children.length).toBe(4);
expect(
fixture.debugElement.query(
......@@ -390,18 +389,32 @@ describe('WireCreatorComponent', () => {
'.m-wire--creator-selector > li:first-child > .m-wire--creator-selector-type > h5 > span'
)
).nativeElement.textContent
).toContain('OnChain');
).toContain('Tokens');
expect(
fixture.debugElement.query(
By.css(
'.m-wire--creator-selector > li:nth-child(2) > .m-wire--creator-selector-type > h5 > span'
)
).nativeElement.textContent
).toContain('OffChain');
).toContain('USD');
expect(
fixture.debugElement.query(
By.css(
'.m-wire--creator-selector > li:nth-child(3) > .m-wire--creator-selector-type > h5 > span'
)
).nativeElement.textContent
).toContain('ETH');
expect(
fixture.debugElement.query(
By.css(
'.m-wire--creator-selector > li:nth-child(4) > .m-wire--creator-selector-type > h5 > span'
)
).nativeElement.textContent
).toContain('BTC');
});
it('clicking on a payment option should highlight it', fakeAsync(() => {
comp.setPayloadType('offchain'); // Select other
comp.setPayloadType('usd'); // Select other
fixture.detectChanges();
tick();
......@@ -475,10 +488,10 @@ describe('WireCreatorComponent', () => {
expect(balance).toBe('500');
});
it(`should have OffChain balance`, () => {
it(`should have token balance`, () => {
fixture.detectChanges();
const onchainOption = getPaymentMethodItem(2),
const onchainOption = getPaymentMethodItem(1),
subtext = onchainOption
.query(By.css('.m-wire--creator-selector-subtext'))
.nativeElement.textContent.trim(),
......@@ -495,10 +508,10 @@ describe('WireCreatorComponent', () => {
});
it(`recurring checkbox should toggle wire's recurring property`, () => {
comp.setPayloadType('onchain');
comp.setPayloadType('offchain');
fixture.detectChanges();
expect(comp.wire.recurring).toBe(false);
expect(comp.wire.recurring).toBe(true);
const checkbox: DebugElement = getRecurringCheckbox();
checkbox.nativeElement.click();
......@@ -507,12 +520,12 @@ describe('WireCreatorComponent', () => {
fixture.detectChanges();
expect(checkbox).not.toBeNull();
expect(comp.wire.recurring).toBe(true);
expect(comp.wire.recurring).toBe(false);
});
it('should show creator rewards', () => {
it('should show creator tiers', () => {
expect(
fixture.debugElement.query(By.css('m-wire--creator-rewards'))
fixture.debugElement.query(By.css('m-wireCreator__rewards'))
).not.toBeNull();
});
......@@ -586,9 +599,17 @@ describe('WireCreatorComponent', () => {
spyOn(comp, 'submit').and.callThrough();
spyOn(comp, 'canSubmit').and.returnValue(true);
// Select tokens
const selectTokens = getPaymentMethodItem(1);
selectTokens.nativeElement.click();
fixture.detectChanges();
// Select onchain method
const select = fixture.debugElement.query(
By.css('.m-wireCreator__tokenMethod .m-selector select')
).nativeElement;
select.value = select.options[0].value;
select.dispatchEvent(new Event('change'));
fixture.detectChanges();
const amountInput: DebugElement = getAmountInput();
......@@ -616,4 +637,59 @@ describe('WireCreatorComponent', () => {
recurring: false,
});
}));
it('should open usd payments when selected', fakeAsync(() => {
// Select usd
const selectTokens = getPaymentMethodItem(2);
selectTokens.nativeElement.click();
fixture.detectChanges();
expect(comp.wire.payloadType).toBe('usd');
const ccSelector = fixture.debugElement.query(
By.css('m-payments__selectCard')
);
expect(ccSelector).not.toBeNull();
}));
it('should update amount and method on tier/reward selection events', fakeAsync(() => {
// selectReward calls the function
comp.setTier({
amount: 5,
currency: 'tokens',
});
fixture.detectChanges();
tick();
// Amount input changes to 5 Tokens
const amountInput: DebugElement = getAmountInput();
expect(amountInput.nativeElement.value).toBe('5');
// Payment method changes to tokens
const tokenOptions = getPaymentMethodItem(1);
expect(
tokenOptions.nativeElement.classList.contains(
'm-wire--creator-selector--highlight'
)
).toBeTruthy();
// selectReward calls the function
comp.setTier({
amount: 15,
currency: 'usd',
});
fixture.detectChanges();
tick();
// Amount input changes to 15 USD
expect(amountInput.nativeElement.value).toBe('15');
// Payment method changes to tokens
const usdOptions = getPaymentMethodItem(2);
expect(
usdOptions.nativeElement.classList.contains(
'm-wire--creator-selector--highlight'
)
).toBeTruthy();
}));
});
......@@ -17,7 +17,13 @@ import { TokenContractService } from '../../blockchain/contracts/token-contract.
import { MindsUser } from '../../../interfaces/entities';
import { Router } from '@angular/router';
export type PayloadType = 'onchain' | 'offchain' | 'creditcard';
export type PayloadType =
| 'onchain'
| 'offchain'
| 'usd'
| 'eth'
| 'erc20'
| 'btc';
export class VisibleWireError extends Error {
visible: boolean = true;
......@@ -44,7 +50,7 @@ export class WireCreatorComponent {
amount: 1,
payloadType: 'onchain',
guid: null,
recurring: false,
recurring: true,
// Payment
payload: null,
......@@ -233,7 +239,7 @@ export class WireCreatorComponent {
setDefaults() {
this.wire.amount = 1;
this.wire.recurring = false;
this.wire.recurring = true;
let payloadType = localStorage.getItem('preferred-payment-method');
if (['onchain', 'offchain'].indexOf(payloadType) === -1) {
payloadType = 'offchain';
......@@ -253,10 +259,14 @@ export class WireCreatorComponent {
this.wire.payload = null;
if (payloadType === 'onchain') {
if (payloadType === 'onchain' || payloadType === 'eth') {
this.setOnchainNoncePayload('');
}
if (payloadType === 'btc') {
this.setBtcNoncePayload('');
}
localStorage.setItem('preferred-payment-method', payloadType);
this.roundAmount();
......@@ -278,6 +288,10 @@ export class WireCreatorComponent {
return this.setNoncePayload({ receiver: this.owner.eth_wallet, address });
}
setBtcNoncePayload(address: string) {
return this.setNoncePayload({ receiver: this.owner.btc_address, address });
}
/**
* Sets the creditcard specific wire payment nonce
*/
......@@ -432,9 +446,16 @@ export class WireCreatorComponent {
}
break;
case 'creditcard':
if (!this.wire.payload) {
throw new Error('Payment method not processed.');
case 'usd':
//if (!this.wire.payload) {
// throw new Error('Payment method not processed.');
//}
break;
case 'btc':
if (!this.wire.payload.receiver) {
throw new VisibleWireError(
'This channel has not configured their Bitcoin address yet'
);
}
break;
}
......@@ -514,7 +535,10 @@ export class WireCreatorComponent {
}
}
let { done } = await this.wireService.submitWire(this.wire);
let { done } = await this.wireService.submitWire({
...this.wire,
...{ recurring: this.wire.recurring && this.canRecur }, // Override when we can't recur but don't change component boolean
});
if (done) {
this.success = true;
......@@ -533,4 +557,33 @@ export class WireCreatorComponent {
this.inProgress = false;
}
}
get canRecur(): boolean {
switch (this.wire.payloadType) {
//case 'onchain':
case 'offchain':
case 'usd':
return true;
}
return false;
}
setUsdPaymentMethod(paymentMethodId) {
this.wire.payload = {
paymentMethodId: paymentMethodId,
};
}
setTier(reward) {
if (!reward) return;
this.wire.amount = reward.amount;
switch (reward.currency) {
case 'tokens':
this.wire.payloadType = 'offchain';
break;
default:
this.wire.payloadType = reward.currency;
}
console.log('setting tier with', this.wire.amount, this.wire.payloadType);
}
}
<div *ngIf="(rewards?.rewards)[type]?.length" class="m-wire--creator-rewards">
<div class="m-wire--creator-rewards--title">
<h3
*ngIf="channel?.username"
class="m-wire--creator-section-title--small"
i18n="@@WIRE__CREATOR__REWARDS__TITLE"
>
{{ channel.username }}'s rewards
</h3>
<div class="m-selector">
<select [ngModel]="selectedReward" (ngModelChange)="selectReward($event)">
<option *ngFor="let reward of rewards" [ngValue]="reward">
{{ reward.amount }}
<ng-container *ngIf="reward.currency == 'tokens'">
{{ reward.amount > 1 ? 'Tokens' : 'Token' }}
</ng-container>
<ng-container *ngIf="reward.currency == 'usd'">
USD
</ng-container>
/ month
</option>
<option [ngValue]="null">Custom subscription</option>
</select>
</div>
<div *ngIf="sums" class="m-wire--creator-rewards--sums">
<ng-container i18n="@@WIRE__CREATOR__REWARDS__YOU_SENT_LABEL"
>You have sent</ng-container
<div>
<div *ngIf="selectedReward && !selectedReward.custom">
<div class="m-wire--creator-rewards--amount">
<span *ngIf="type == 'money'">{{
selectedReward.amount | currency: 'USD':true:'1.0-0'
}}</span>
<span *ngIf="type == 'points'" i18n="@@M__COMMON__POINTS_WITH_VALUE"
>{{ selectedReward.amount | number }} points</span
>
<b *ngIf="type == 'points'" i18n="@@M__COMMON__POINTS_WITH_VALUE">
{{ sums.points | number }} points
</b>
<b *ngIf="type == 'money'">
{{ sums.money | currency: 'USD':true:'1.0-0' }}
</b>
<b *ngIf="type == 'tokens'">
{{ sums.tokens | number: '1.0-4' }}
</b>
<ng-container i18n="@@WIRE__CREATOR__REWARDS__IN_THE_LAST_MONTH"
>in the last month.</ng-container
<span *ngIf="type == 'tokens'" i18n="@@M__COMMON__TOKENS_WITH_VALUE"
>{{ selectedReward.amount | number }} Tokens</span
>
</div>
<div class="m-wire--creator-rewards--description">
<b
>{{ selectedReward.amount }}
<ng-container *ngIf="selectedReward.currency === 'usd'"
>USD</ng-container
>
<ng-container *ngIf="selectedReward.currency === 'tokens'">{{
selectedReward.amount > 1 ? 'Tokens' : 'Token'
}}</ng-container>
</b>
-
<p>
{{
selectedReward.description
? selectedReward.description
: 'No description'
}}
</p>
</div>
</div>
<ul class="m-wire--creator-rewards--list">
<ng-container *ngFor="let reward of rewards.rewards[type]; let i = index">
<li
class="m-wire--creator-rewards--threshold"
[class.m-wire--creator-rewards--above-threshold]="
isRewardAboveThreshold(i)
"
[class.m-wire--creator-rewards--best-reward]="isBestReward(i)"
(click)="selectReward(i)"
>
<div class="m-wire--creator-rewards--amount">
<span *ngIf="type == 'money'">{{
reward.amount | currency: 'USD':true:'1.0-0'
}}</span>
<span *ngIf="type == 'points'" i18n="@@M__COMMON__POINTS_WITH_VALUE"
>{{ reward.amount | number }} points</span
>
<span *ngIf="type == 'tokens'" i18n="@@M__COMMON__TOKENS_WITH_VALUE"
>{{ reward.amount | number }} Tokens</span
>
</div>
<div class="m-wire--creator-rewards--description">
<p>{{ reward.description }}</p>
</div>
</li>
</ng-container>
</ul>
</div>
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { FeaturesService } from '../../../../services/features.service';
import {
WireRewardsType,
WireRewardsStruc,
......@@ -7,48 +8,92 @@ import {
} from '../../interfaces/wire.interfaces';
@Component({
moduleId: module.id,
selector: 'm-wire--creator-rewards',
selector: 'm-wireCreator__rewards',
templateUrl: 'rewards.component.html',
})
export class WireCreatorRewardsComponent {
@Input() rewards: WireRewardsStruc;
rewards: Array<any> = [];
@Input() amount: number = 1;
currency: string = 'tokens';
@Input() type: WireRewardsType | null;
@Input() amount: string | number;
@Input() channel: any;
@Input() sums: any;
@Output() selectAmount: EventEmitter<any> = new EventEmitter(true);
@Output() selectCurrency: EventEmitter<string> = new EventEmitter(true);
@Output('selectReward') selectRewardEvt: EventEmitter<any> = new EventEmitter(
true
);
isRewardAboveThreshold(index: number): boolean {
if (!this.rewards || !this.type || !this.calcAmount()) {
return false;
}
constructor(private featuresService: FeaturesService) {}
return this.calcAmount() >= this.rewards.rewards[this.type][index].amount;
selectReward(reward): void {
this.selectRewardEvt.next(reward);
// this.selectAmount.next(reward.amount);
//this.selectCurrency.next(reward.currency);
}
isBestReward(index: number): boolean {
if (!this.rewards || !this.type || !this.calcAmount()) {
return false;
get selectedReward() {
const methods = [{ method: 'tokens', currency: 'offchain' }];
if (this.featuresService.has('wire-multi-currency')) {
methods.push({ method: 'money', currency: 'usd' });
}
for (const method of methods) {
const match = this.findReward();
if (match) {
//this.selectReward(match);
return match;
break;
}
}
return null;
}
const lastEligibleReward = this.rewards.rewards[this.type]
.map((reward, index) => ({ ...reward, index }))
.filter(reward => this.calcAmount() >= reward.amount)
.pop();
/**
* Return a reward that closest matches our query
* @param method
*/
return lastEligibleReward ? index === lastEligibleReward.index : false;
private findReward() {
for (let r of this.rewards) {
if (this.currency === r.currency && this.amount == r.amount) {
return r;
}
}
return null;
}
calcAmount(): number {
if (this.sums && this.sums[this.type]) {
return parseFloat(this.sums[this.type]) + parseFloat(<string>this.amount);
@Input('rewards') set _rewards(rewards) {
this.rewards = [];
const methodsMap = [{ method: 'tokens', currency: 'tokens' }];
if (this.featuresService.has('wire-multi-currency')) {
methodsMap.push({ method: 'money', currency: 'usd' });
}
return <number>this.amount;
for (const { method, currency } of methodsMap) {
for (const reward of rewards.rewards[method]) {
this.rewards.push({
amount: parseInt(reward.amount),
description: reward.description,
currency,
});
}
}
}
selectReward(index: number): void {
this.selectAmount.next(this.rewards.rewards[this.type][index].amount);
@Input('currency') set _currency(currency: string) {
switch (currency) {
//case 'money':
//currency = 'usd';
// break;
case 'offchain':
case 'onchain':
currency = 'tokens';
break;
}
this.currency = currency;
}
}
......@@ -6,6 +6,7 @@ import { CommonModule } from '../../common/common.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CheckoutModule } from '../checkout/checkout.module';
import { FaqModule } from '../faq/faq.module';
import { PaymentsModule } from '../payments/payments.module';
import { WireCreatorComponent } from './creator/creator.component';
import { WirePaymentsCreatorComponent } from './payments-creator/creator.component';
......@@ -39,6 +40,7 @@ const wireRoutes: Routes = [
CommonModule,
CheckoutModule,
FaqModule,
PaymentsModule,
],
declarations: [
WireLockScreenComponent,
......
......@@ -4,6 +4,7 @@ import { wireContractServiceMock } from '../../../tests/wire-contract-service-mo
import { tokenContractServiceMock } from '../../../tests/token-contract-service-mock.spec';
import { web3WalletServiceMock } from '../../../tests/web3-wallet-service-mock.spec';
import { fakeAsync, tick } from '@angular/core/testing';
import { BTCService } from '../payments/btc/btc.service';
describe('WireService', () => {
let service: WireService;
......@@ -15,7 +16,8 @@ describe('WireService', () => {
clientMock,
wireContractServiceMock,
tokenContractServiceMock,
web3WalletServiceMock
web3WalletServiceMock,
new (() => {})()
);
clientMock.response = {};
......@@ -46,7 +48,7 @@ describe('WireService', () => {
expect(web3WalletServiceMock.getCurrentWallet).toHaveBeenCalled();
expect(clientMock.post).toHaveBeenCalled();
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v1/wire/null`);
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v2/wire/null`);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual({
amount: 10,
payload: {
......@@ -55,7 +57,7 @@ describe('WireService', () => {
method: 'onchain',
txHash: 'hash',
},
method: 'tokens',
method: 'onchain',
recurring: false,
});
}));
......@@ -72,36 +74,36 @@ describe('WireService', () => {
tick();
expect(clientMock.post).toHaveBeenCalled();
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v1/wire/null`);
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v2/wire/null`);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual({
amount: 10,
payload: { address: 'offchain', method: 'offchain' },
method: 'tokens',
method: 'offchain',
recurring: false,
});
}));
it('should submit a credit card wire', fakeAsync(() => {
it('should submit a usd wire', fakeAsync(() => {
service.submitWire({
amount: 10,
guid: null,
payload: { address: 'offchain', token: 'tok_KPte7942xySKBKyrBu11yEpf' },
payloadType: 'creditcard',
payloadType: 'usd',
recurring: false,
});
tick();
expect(clientMock.post).toHaveBeenCalled();
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v1/wire/null`);
expect(clientMock.post.calls.mostRecent().args[0]).toBe(`api/v2/wire/null`);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual({
amount: 10,
payload: {
address: 'offchain',
token: 'tok_KPte7942xySKBKyrBu11yEpf',
method: 'creditcard',
method: 'usd',
},
method: 'tokens',
method: 'usd',
recurring: false,
});
}));
......
......@@ -4,6 +4,7 @@ import { WireContractService } from '../blockchain/contracts/wire-contract.servi
import { TokenContractService } from '../blockchain/contracts/token-contract.service';
import { Web3WalletService } from '../blockchain/web3-wallet.service';
import { WireStruc } from './creator/creator.component';
import { BTCService } from '../payments/btc/btc.service';
@Injectable()
export class WireService {
......@@ -13,7 +14,8 @@ export class WireService {
private client: Client,
private wireContract: WireContractService,
private tokenContract: TokenContractService,
private web3Wallet: Web3WalletService
private web3Wallet: Web3WalletService,
private btcService: BTCService
) {}
async submitWire(wire: WireStruc) {
......@@ -64,19 +66,48 @@ export class WireService {
}
break;
case 'creditcard':
payload.method = 'creditcard';
case 'eth':
await this.web3Wallet.ready();
if (this.web3Wallet.isUnavailable()) {
throw new Error('No Ethereum wallets available on your browser.');
} else if (!(await this.web3Wallet.unlock())) {
throw new Error(
'Your Ethereum wallet is locked or connected to another network.'
);
}
await this.web3Wallet.sendTransaction({
from: await this.web3Wallet.getCurrentWallet(),
to: payload.receiver,
gasPrice: this.web3Wallet.EthJS.toWei(2, 'Gwei'),
gas: 21000,
value: this.web3Wallet.EthJS.toWei(wire.amount, 'ether').toString(),
data: '0x',
});
break;
case 'usd':
payload.method = 'usd';
break;
case 'offchain':
payload = { method: 'offchain', address: 'offchain' };
break;
case 'btc':
this.btcService.showModal({
amount: wire.amount,
address: wire.payload.receiver,
});
return;
break;
}
try {
let response: any = await this.client.post(`api/v1/wire/${wire.guid}`, {
let response: any = await this.client.post(`api/v2/wire/${wire.guid}`, {
payload,
method: 'tokens',
method: payload.method,
amount: wire.amount,
recurring: wire.recurring,
});
......