import * as i0 from '@angular/core'; import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, booleanAttribute, output, signal, DestroyRef, Component } from '@angular/core'; import * as QRCode from 'qrcode'; import { catchError, throwError, timer, switchMap } from 'rxjs'; import { HttpHeaders, HttpClient, HttpErrorResponse } from '@angular/common/http'; const MARKETPLACES_PAYMENT_CONFIG = new InjectionToken('@marketplaces/payment config'); function provideMarketplacesPayment(config) { return makeEnvironmentProviders([{ provide: MARKETPLACES_PAYMENT_CONFIG, useValue: { ...config, apiUrl: config.apiUrl.replace(/\/$/, ''), paymentsPath: config.paymentsPath ?? '/api/v1/payments', pollIntervalMs: config.pollIntervalMs ?? 1500, }, }]); } const MARKETPLACE_DOMAIN_HEADER = 'X-Marketplace-Domain'; function normalizeMarketplaceDomain(domain) { return domain.trim().toLowerCase().replace(/\.$/, ''); } class PaymentMarketplaceContext { constructor() { this.config = inject(MARKETPLACES_PAYMENT_CONFIG); } domain() { const configured = this.config.marketplaceDomain; const domain = typeof configured === 'function' ? configured() : configured ?? (typeof location === 'undefined' ? '' : location.hostname); return normalizeMarketplaceDomain(domain); } headers() { const domain = this.domain(); if (!domain) throw new Error('Marketplace domain cannot be resolved'); return new HttpHeaders({ [MARKETPLACE_DOMAIN_HEADER]: domain }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMarketplaceContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMarketplaceContext, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PaymentMarketplaceContext, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const MARKETPLACES_PAYMENT_GATEWAY = new InjectionToken('@marketplaces/payment gateway', { providedIn: 'root', factory: () => inject(HttpMarketplacesPaymentGateway) }); class HttpMarketplacesPaymentGateway { constructor() { this.http = inject(HttpClient); this.config = inject(MARKETPLACES_PAYMENT_CONFIG); this.context = inject(PaymentMarketplaceContext); } create(method, request) { return this.http.post(this.baseUrl(), { ...request, method }, { headers: this.context.headers() }).pipe(this.handle(method)); } status(paymentId, method) { return this.http.get(`${this.baseUrl()}/${encodeURIComponent(paymentId)}`, { headers: this.context.headers() }).pipe(this.handle(method)); } cancel(paymentId) { return this.http.post(`${this.baseUrl()}/${encodeURIComponent(paymentId)}/cancel`, {}, { headers: this.context.headers() }); } baseUrl() { const path = this.config.paymentsPath ?? ''; return `${this.config.apiUrl}${path.startsWith('/') ? path : `/${path}`}`; } handle(method) { return (source) => source.pipe(catchError(cause => { const response = cause instanceof HttpErrorResponse ? cause : null; const failure = { method, code: response?.status === 402 ? 'declined' : 'backend', message: response?.error?.message || response?.message || 'Payment failed', cause, }; return throwError(() => failure); })); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesPaymentGateway, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesPaymentGateway, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HttpMarketplacesPaymentGateway, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class MarketplacesPaymentComponent { constructor() { this.qr = input(false, { ...(ngDevMode ? { debugName: "qr" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); this.card = input(false, { ...(ngDevMode ? { debugName: "card" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); this.sbp = input(false, { ...(ngDevMode ? { debugName: "sbp" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); this.yandexPay = input(false, { ...(ngDevMode ? { debugName: "yandexPay" } : /* istanbul ignore next */ {}), transform: booleanAttribute }); this.request = input.required(/* @ts-ignore */ ...(ngDevMode ? [{ debugName: "request" }] : /* istanbul ignore next */ [])); this.title = input('Оплата', /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ [])); this.completed = output(); this.paymentError = output(); this.cancelled = output(); this.busy = signal(false, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ [])); this.error = signal(null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ [])); this.attempt = signal(null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "attempt" }] : /* istanbul ignore next */ [])); this.qrImage = signal(null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "qrImage" }] : /* istanbul ignore next */ [])); this.actionUrl = signal(null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "actionUrl" }] : /* istanbul ignore next */ [])); this.gateway = inject(MARKETPLACES_PAYMENT_GATEWAY); this.config = inject(MARKETPLACES_PAYMENT_CONFIG); inject(DestroyRef).onDestroy(() => this.poll?.unsubscribe()); } pay(method) { this.poll?.unsubscribe(); this.error.set(null); this.busy.set(true); this.qrImage.set(null); this.gateway.create(method, this.request()).subscribe({ next: created => void this.handleCreated(created).catch(cause => this.fail(method, cause)), error: cause => this.fail(method, cause), }); } cancel() { const current = this.attempt(); this.poll?.unsubscribe(); this.busy.set(false); this.attempt.set(null); if (current) this.gateway.cancel(current.paymentId).subscribe({ error: () => undefined }); this.cancelled.emit(); } async handleCreated(created) { this.attempt.set(created); if (created.action?.url) { this.actionUrl.set(created.action.url); if (created.action.type === 'qr') this.qrImage.set(await QRCode.toDataURL(created.action.url, { width: 320, margin: 1 })); else if (typeof window !== 'undefined' && !window.open(created.action.url, '_blank', 'noopener')) { this.fail(created.method, { method: created.method, code: 'popup_blocked', message: 'Браузер заблокировал окно оплаты' }); return; } } if (created.status === 'paid' || created.status === 'authorized') { this.finish(created); return; } if (created.status === 'failed' || created.status === 'expired') { this.fail(created.method, created.failureMessage ?? 'Платёж отклонён'); return; } this.poll = timer(0, this.config.pollIntervalMs ?? 1500).pipe(switchMap(() => this.gateway.status(created.paymentId, created.method))) .subscribe({ next: status => this.handleStatus(status), error: cause => this.fail(created.method, cause) }); } handleStatus(current) { this.attempt.set(current); if (current.status === 'paid' || current.status === 'authorized') this.finish(current); else if (current.status === 'failed' || current.status === 'expired' || current.status === 'cancelled') this.fail(current.method, current.failureMessage ?? 'Платёж не завершён'); } finish(attempt) { this.poll?.unsubscribe(); this.busy.set(false); this.attempt.set(null); this.completed.emit(attempt); } fail(method, cause) { this.poll?.unsubscribe(); this.busy.set(false); const failure = this.isFailure(cause) ? cause : { method, code: 'backend', message: typeof cause === 'string' ? cause : 'Не удалось провести платёж', cause }; this.error.set(failure); this.paymentError.emit(failure); } isFailure(value) { return !!value && typeof value === 'object' && 'code' in value && 'message' in value; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MarketplacesPaymentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: MarketplacesPaymentComponent, isStandalone: true, selector: "mp-payment, marketplaces-payment", inputs: { qr: { classPropertyName: "qr", publicName: "qr", isSignal: true, isRequired: false, transformFunction: null }, card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: false, transformFunction: null }, sbp: { classPropertyName: "sbp", publicName: "sbp", isSignal: true, isRequired: false, transformFunction: null }, yandexPay: { classPropertyName: "yandexPay", publicName: "yandexPay", isSignal: true, isRequired: false, transformFunction: null }, request: { classPropertyName: "request", publicName: "request", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { completed: "completed", paymentError: "paymentError", cancelled: "cancelled" }, ngImport: i0, template: ` {{ title() }} @if (qr()) { QR } @if (card()) { Карта } @if (sbp()) { СБП } @if (yandexPay()) { Яндекс Pay } @if (qrImage()) { } @if (busy()) { Проверяем оплату… } @if (error()) { {{ error()!.message }} } @if (attempt()) { Отменить } `, isInline: true, styles: [":host{display:block}.mp-payment{font:inherit;color:inherit;display:grid;gap:1rem;max-width:26rem}h2,p{margin:0}.methods{display:flex;gap:.6rem;flex-wrap:wrap}button{font:inherit;cursor:pointer;border:1px solid #111;border-radius:.65rem;padding:.75rem 1rem;background:#111;color:#fff}button:disabled{opacity:.55;cursor:wait}.cancel{background:transparent;color:inherit;border-color:#c7c7c7}img{display:block;width:min(15rem,100%);height:auto}.error{color:#b42318}\n"] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: MarketplacesPaymentComponent, decorators: [{ type: Component, args: [{ selector: 'mp-payment, marketplaces-payment', standalone: true, template: ` {{ title() }} @if (qr()) { QR } @if (card()) { Карта } @if (sbp()) { СБП } @if (yandexPay()) { Яндекс Pay } @if (qrImage()) { } @if (busy()) { Проверяем оплату… } @if (error()) { {{ error()!.message }} } @if (attempt()) { Отменить } `, styles: [":host{display:block}.mp-payment{font:inherit;color:inherit;display:grid;gap:1rem;max-width:26rem}h2,p{margin:0}.methods{display:flex;gap:.6rem;flex-wrap:wrap}button{font:inherit;cursor:pointer;border:1px solid #111;border-radius:.65rem;padding:.75rem 1rem;background:#111;color:#fff}button:disabled{opacity:.55;cursor:wait}.cancel{background:transparent;color:inherit;border-color:#c7c7c7}img{display:block;width:min(15rem,100%);height:auto}.error{color:#b42318}\n"] }] }], ctorParameters: () => [], propDecorators: { qr: [{ type: i0.Input, args: [{ isSignal: true, alias: "qr", required: false }] }], card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: false }] }], sbp: [{ type: i0.Input, args: [{ isSignal: true, alias: "sbp", required: false }] }], yandexPay: [{ type: i0.Input, args: [{ isSignal: true, alias: "yandexPay", required: false }] }], request: [{ type: i0.Input, args: [{ isSignal: true, alias: "request", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], paymentError: [{ type: i0.Output, args: ["paymentError"] }], cancelled: [{ type: i0.Output, args: ["cancelled"] }] } }); /** * Generated bundle index. Do not edit. */ export { HttpMarketplacesPaymentGateway, MARKETPLACES_PAYMENT_CONFIG, MARKETPLACES_PAYMENT_GATEWAY, MARKETPLACE_DOMAIN_HEADER, MarketplacesPaymentComponent, PaymentMarketplaceContext, normalizeMarketplaceDomain, provideMarketplacesPayment }; //# sourceMappingURL=marketplaces-payment.mjs.map
Проверяем оплату…
{{ error()!.message }}