Commit 3ccd8be8 authored by Mark Harding's avatar Mark Harding

(feat): various changes to introduce USD option to wire'

No related merge requests found
Pipeline #72503119 failed with stages
in 6 minutes and 36 seconds
......@@ -65,4 +65,43 @@ minds-button-thumbs-down {
color: rgba(themed($m-blue-dark),0.9) !important;
}
}
}
\ No newline at end of file
}
.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);
}
}
}
<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,14 @@ 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';
@NgModule({
imports: [
NgCommonModule,
FormsModule,
ReactiveFormsModule,
CommonModule,
CheckoutModule,
ModalsModule
......@@ -19,11 +23,18 @@ import { PaywallCancelButton } from './paywall/paywall-cancel.component';
declarations: [
PayWall,
PaywallCancelButton,
PaymentsNewCard,
PaymentsSelectCard,
],
exports: [
PayWall,
PaywallCancelButton,
]
PaymentsNewCard,
PaymentsSelectCard,
],
entryComponents: [
PaymentsNewCard,
],
})
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<void> = 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>
......@@ -11,12 +11,24 @@
<li *ngFor="let card of cards; let i = index"
class="m-settings--billing-saved-cards--cards-item"
>
<span class="m-settings--billing-saved-cards--cards-item-type">{{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>
<span class="m-settings--billing-saved-cards--cards-item-type">{{card.card_brand}}</span>
<span class="m-settings--billing-saved-cards--cards-item-number">**** {{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)" i18n="@@M__ACTION__REMOVE">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',
......@@ -12,37 +13,30 @@ 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> {
this.inProgress = true;
this.cards = [];
return this.client.get(`api/v1/payments/stripe/cards`)
.then(({ cards }) => {
return this.client.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();
}
})
......@@ -55,7 +49,7 @@ export class SettingsBillingSavedCardsComponent {
removeCard(index: number) {
this.inProgress = true;
this.client.delete('api/v1/payments/stripe/card/' + this.cards[index].id)
this.client.delete('api/v2/payments/stripe/paymentmethods/' + this.cards[index].id)
.then(() => {
this.cards.splice(index, 1);
......@@ -68,50 +62,14 @@ 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({
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);
});
});
addNewCard() {
this.overlayModal.create(PaymentsNewCard, {}, {
class: '',
onCompleted: () => {
this.loadSavedCards(); //refresh list
this.overlayModal.dismiss();
},
}).present();
}
detectChanges(): void {
......
......@@ -88,6 +88,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;
}
}
......@@ -394,16 +400,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;
......@@ -654,7 +660,7 @@
// }
&:not(.m-wire--creator-selector--highlight) .m-wire--creator-selector-type{
> i, h5 > span,
> i, h5 > span, img,
.m-boost--creator-selector--hoverable,
.material-icons {
opacity: 0.5;
......
......@@ -11,7 +11,7 @@ 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';
export class VisibleWireError extends Error {
visible: boolean = true;
......@@ -244,7 +244,7 @@ export class WireCreatorComponent {
this.wire.payload = null;
if (payloadType === 'onchain') {
if (payloadType === 'onchain' || payloadType === 'eth') {
this.setOnchainNoncePayload('');
}
......@@ -416,10 +416,10 @@ 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;
}
......@@ -514,4 +514,21 @@ 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,
};
}
}
......@@ -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,
......
......@@ -55,8 +55,27 @@ 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':
......@@ -65,9 +84,9 @@ export class WireService {
}
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
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment