Skip to content

Commit

Permalink
feat(elements): add supoprt for Issuing Elements
Browse files Browse the repository at this point in the history
  • Loading branch information
richnologies committed Feb 4, 2023
1 parent 69da6ac commit e71dae9
Show file tree
Hide file tree
Showing 10 changed files with 633 additions and 1 deletion.
5 changes: 5 additions & 0 deletions projects/ngx-stripe-docs/src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export const ROUTES: Routes = [
loadComponent: () => import('./examples/iban-element.component').then(m => m.IbanElementExampleComponent),
data: { title: 'IBAN Example' }
},
{
path: 'examples/issuing-elements',
loadComponent: () => import('./examples/issuing-elements.component').then(m => m.IssuingElementsExampleComponent),
data: { title: 'Issuing Elements Example' }
},
{
path: 'examples/link-authentication-element',
loadComponent: () => import('./examples/link-authentication-element.component').then(m => m.LinkAuthenticationElementExampleComponent),
Expand Down
9 changes: 9 additions & 0 deletions projects/ngx-stripe-docs/src/app/core/pluto/pluto.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,13 @@ export class NgStrPlutoService {
{ headers: { merchant: options.clientId ?? this.clientId } }
);
}

createEphemeralKeys(
params: Stripe.EphemeralKeyCreateParams,
options: { clientId?: string } = {}
): Observable<{ ephemeralKeySecret: string }> {
return this.http.post<{ ephemeralKeySecret: string }>(`${this.BASE_URL}/payments/ephemeral-keys`, params, {
headers: { merchant: options.clientId ?? this.clientId }
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, ViewChild } from '@angular/core';
import { switchMap } from 'rxjs';

import { StripeFactoryService, StripeElementsDirective, StripeIssuingCardNumberDisplayComponent, StripeIssuingCardCvcDisplayComponent, StripeIssuingCardExpiryDisplayComponent, StripeIssuingCardPinDisplayComponent } from 'ngx-stripe';
import { StripeElementsOptions, StripeIssuingCardNumberDisplayElement, StripeIssuingCardNumberDisplayElementOptions } from '@stripe/stripe-js';

import { NgStrPlutoService } from '../core';

@Component({
selector: 'ngstr-issuing-elements-example',
template: `
<div>
<div color="secondary" section-content-header>
<span>One Card Example</span>
</div>
<div section-content>
<ngx-stripe-elements [stripe]="stripe" [elementsOptions]="elementsOptions" *ngIf="cardOptions">
<ngx-stripe-issuing-card-number-display [options]="cardOptions"
></ngx-stripe-issuing-card-number-display>
<br />
<ngx-stripe-issuing-card-expiry-display [options]="cardOptions"
></ngx-stripe-issuing-card-expiry-display>
<br />
<ngx-stripe-issuing-card-cvc-display [options]="cardOptions"
></ngx-stripe-issuing-card-cvc-display>
<br />
<ngx-stripe-issuing-card-pin-display [options]="cardOptions"
></ngx-stripe-issuing-card-pin-display>
</ngx-stripe-elements>
</div>
</div>
`,
styles: [],
standalone: true,
imports: [
CommonModule,
StripeElementsDirective,
StripeIssuingCardCvcDisplayComponent,
StripeIssuingCardExpiryDisplayComponent,
StripeIssuingCardNumberDisplayComponent,
StripeIssuingCardPinDisplayComponent
]
})
export class IssuingElementsExampleComponent implements OnInit {
@ViewChild('card') card: StripeIssuingCardNumberDisplayElement;

cardId = 'ic_1MXkbPCFzZvO65bFj561Zea7';
nonce: string;
ek: string;

stripe = this.stripeFactory.create(this.plutoService.KEYS.usa);
cardOptions: StripeIssuingCardNumberDisplayElementOptions;
elementsOptions: StripeElementsOptions = {
locale: 'es'
};

constructor(
private stripeFactory: StripeFactoryService,
private plutoService: NgStrPlutoService
) {}

ngOnInit() {
this.stripe.createEphemeralKeyNonce({ issuingCard: this.cardId })
.pipe(
switchMap(({ nonce }) => {
this.nonce = nonce;
const data: any = { issuing_card: this.cardId, nonce };
return this.plutoService.createEphemeralKeys(data, { clientId: 'b3eb5880-ee40-4c96-9356-cb43da836aa6' });
})
)
.subscribe({
next: ({ ephemeralKeySecret }) => {
this.ek = ephemeralKeySecret;
this.cardOptions = {
issuingCard: this.cardId,
nonce: this.nonce,
ephemeralKeySecret: this.ek,
style: {
base: {
color: '#000',
fontSize: '16px'
},
},
};
},
error: err => console.error(err)
});
}
}
127 changes: 127 additions & 0 deletions projects/ngx-stripe/src/lib/components/issuing-card-cvc.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { CommonModule } from '@angular/common';
import {
Component,
Input,
ViewChild,
ElementRef,
EventEmitter,
Output,
OnInit,
OnChanges,
SimpleChanges,
OnDestroy,
ContentChild,
TemplateRef,
Optional,
ChangeDetectorRef
} from '@angular/core';
import { lastValueFrom, Subscription } from 'rxjs';

import {
StripeElementsOptions,
StripeElements,
StripeIssuingCardCvcDisplayElement,
StripeIssuingCardCvcDisplayElementOptions
} from '@stripe/stripe-js';

import { NgxStripeElementLoadingTemplateDirective } from '../directives/stripe-element-loading-template.directive';
import { StripeElementsDirective } from '../directives/elements.directive';

import { StripeInstance } from '../services/stripe-instance.class';
import { StripeElementsService } from '../services/stripe-elements.service';

@Component({
selector: 'ngx-stripe-issuing-card-cvc-display',
standalone: true,
template: `
<div class="field" #stripeElementRef>
<ng-container *ngIf="state !== 'ready' && loadingTemplate" [ngTemplateOutlet]="loadingTemplate"></ng-container>
</div>
`,
imports: [CommonModule]
})
export class StripeIssuingCardCvcDisplayComponent implements OnInit, OnChanges, OnDestroy {
@ContentChild(NgxStripeElementLoadingTemplateDirective, { read: TemplateRef })
loadingTemplate?: TemplateRef<NgxStripeElementLoadingTemplateDirective>;
@ViewChild('stripeElementRef') public stripeElementRef!: ElementRef;
element!: StripeIssuingCardCvcDisplayElement;

@Input() containerClass: string;
@Input() options: StripeIssuingCardCvcDisplayElementOptions;
@Input() elementsOptions: Partial<StripeElementsOptions>;
@Input() stripe: StripeInstance;

@Output() load = new EventEmitter<StripeIssuingCardCvcDisplayElement>();

elements: StripeElements;
state: 'notready' | 'starting' | 'ready' = 'notready';
private elementsSubscription: Subscription;

constructor(
private cdr: ChangeDetectorRef,
public stripeElementsService: StripeElementsService,
@Optional() private elementsProvider: StripeElementsDirective
) {}

async ngOnChanges(changes: SimpleChanges) {
this.state = 'starting';
let updateElements = false;

if (!this.elementsProvider && (changes.elementsOptions || changes.stripe || !this.elements)) {
this.elements = await lastValueFrom(this.stripeElementsService.elements(this.stripe, this.elementsOptions));
updateElements = true;
}

const options = this.stripeElementsService.mergeOptions(this.options, this.containerClass);
if (changes.options || changes.containerClass || !this.element || updateElements) {
if (this.element && !updateElements) {
this.update(options);
} else if (this.elements && updateElements) {
this.createElement(options);
}
}
}

async ngOnInit() {
const options = this.stripeElementsService.mergeOptions(this.options, this.containerClass);

if (this.elementsProvider) {
this.elementsSubscription = this.elementsProvider.elements.subscribe((elements) => {
this.elements = elements;
this.createElement(options);
});
} else if (this.state === 'notready') {
this.state = 'starting';

this.elements = await lastValueFrom(this.stripeElementsService.elements(this.stripe));
this.createElement(options);
}
}

ngOnDestroy() {
if (this.element) {
this.element.destroy();
}
if (this.elementsSubscription) {
this.elementsSubscription.unsubscribe();
}
}

update(options: Partial<StripeIssuingCardCvcDisplayElementOptions>) {
this.element.update(options);
}

private createElement(options: StripeIssuingCardCvcDisplayElementOptions) {
this.state = 'ready';
this.cdr.detectChanges();

if (this.element) {
this.element.unmount();
}

this.element = this.elements.create('issuingCardCvcDisplay', options);
this.element.mount(this.stripeElementRef.nativeElement);

this.load.emit(this.element);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { CommonModule } from '@angular/common';
import {
Component,
Input,
ViewChild,
ElementRef,
EventEmitter,
Output,
OnInit,
OnChanges,
SimpleChanges,
OnDestroy,
ContentChild,
TemplateRef,
Optional,
ChangeDetectorRef
} from '@angular/core';
import { lastValueFrom, Subscription } from 'rxjs';

import {
StripeElementsOptions,
StripeElements,
StripeIssuingCardExpiryDisplayElement,
StripeIssuingCardExpiryDisplayElementOptions
} from '@stripe/stripe-js';

import { NgxStripeElementLoadingTemplateDirective } from '../directives/stripe-element-loading-template.directive';
import { StripeElementsDirective } from '../directives/elements.directive';

import { StripeInstance } from '../services/stripe-instance.class';
import { StripeElementsService } from '../services/stripe-elements.service';

@Component({
selector: 'ngx-stripe-issuing-card-expiry-display',
standalone: true,
template: `
<div class="field" #stripeElementRef>
<ng-container *ngIf="state !== 'ready' && loadingTemplate" [ngTemplateOutlet]="loadingTemplate"></ng-container>
</div>
`,
imports: [CommonModule]
})
export class StripeIssuingCardExpiryDisplayComponent implements OnInit, OnChanges, OnDestroy {
@ContentChild(NgxStripeElementLoadingTemplateDirective, { read: TemplateRef })
loadingTemplate?: TemplateRef<NgxStripeElementLoadingTemplateDirective>;
@ViewChild('stripeElementRef') public stripeElementRef!: ElementRef;
element!: StripeIssuingCardExpiryDisplayElement;

@Input() containerClass: string;
@Input() options: StripeIssuingCardExpiryDisplayElementOptions;
@Input() elementsOptions: Partial<StripeElementsOptions>;
@Input() stripe: StripeInstance;

@Output() load = new EventEmitter<StripeIssuingCardExpiryDisplayElement>();

elements: StripeElements;
state: 'notready' | 'starting' | 'ready' = 'notready';
private elementsSubscription: Subscription;

constructor(
private cdr: ChangeDetectorRef,
public stripeElementsService: StripeElementsService,
@Optional() private elementsProvider: StripeElementsDirective
) {}

async ngOnChanges(changes: SimpleChanges) {
this.state = 'starting';
let updateElements = false;

if (!this.elementsProvider && (changes.elementsOptions || changes.stripe || !this.elements)) {
this.elements = await lastValueFrom(this.stripeElementsService.elements(this.stripe, this.elementsOptions));
updateElements = true;
}

const options = this.stripeElementsService.mergeOptions(this.options, this.containerClass);
if (changes.options || changes.containerClass || !this.element || updateElements) {
if (this.element && !updateElements) {
this.update(options);
} else if (this.elements && updateElements) {
this.createElement(options);
}
}
}

async ngOnInit() {
const options = this.stripeElementsService.mergeOptions(this.options, this.containerClass);

if (this.elementsProvider) {
this.elementsSubscription = this.elementsProvider.elements.subscribe((elements) => {
this.elements = elements;
this.createElement(options);
});
} else if (this.state === 'notready') {
this.state = 'starting';

this.elements = await lastValueFrom(this.stripeElementsService.elements(this.stripe));
this.createElement(options);
}
}

ngOnDestroy() {
if (this.element) {
this.element.destroy();
}
if (this.elementsSubscription) {
this.elementsSubscription.unsubscribe();
}
}

update(options: Partial<StripeIssuingCardExpiryDisplayElementOptions>) {
this.element.update(options);
}

private createElement(options: StripeIssuingCardExpiryDisplayElementOptions) {
this.state = 'ready';
this.cdr.detectChanges();

if (this.element) {
this.element.unmount();
}

this.element = this.elements.create('issuingCardExpiryDisplay', options);
this.element.mount(this.stripeElementRef.nativeElement);

this.load.emit(this.element);
}
}
Loading

0 comments on commit e71dae9

Please sign in to comment.