Merge pull request #6520 from mempool/mononaut/partner-codes
Some checks failed
Backend Integration Tests with MariaDB / Backend Integration Tests - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (dev) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Backend (prod) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Cache assets for builds (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Validate generated backend Docker JSON (push) Has been cancelled
Supply Chain Audit / Backend install-script audit (push) Has been cancelled
Supply Chain Audit / Frontend install-script audit (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (dev) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / Frontend (prod) - node 24.13.0 (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for liquid (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for mempool (push) Has been cancelled
CI Pipeline for the Backend and Frontend / E2E tests for testnet4 (push) Has been cancelled

refactor partnerCode handling
This commit is contained in:
mononaut 2026-06-14 18:33:00 +09:00 committed by GitHub
commit ffb913da09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 127 additions and 34 deletions

View file

@ -10,9 +10,9 @@ import { Transaction } from '@interfaces/electrs.interface';
import { MiningStats } from '@app/services/mining.service';
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { PartnerCodeService } from '@app/services/partner-code.service';
import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core';
import { StorageService } from '@app/services/storage.service';
import { ThemeService } from '../../services/theme.service';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
@ -146,7 +146,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private cd: ChangeDetectorRef,
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
private storageService: StorageService,
private partnerCodeService: PartnerCodeService,
private themeService: ThemeService,
) {
this.isProdDomain = this.stateService.isProdDomain;
@ -562,8 +562,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.partnerCode
).subscribe({
next: (response) => {
this.storageService.removeItem('partnerCode'); // Consume localStorage partnerCode
this.partnerCode = undefined;
this.partnerCodeService.clearStoredPartnerCode();
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
@ -687,8 +686,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.partnerCode
).subscribe({
next: (response) => {
this.storageService.removeItem('partnerCode'); // Consume localStorage partnerCode
this.partnerCode = undefined;
this.partnerCodeService.clearStoredPartnerCode();
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
@ -792,8 +790,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.partnerCode
).subscribe({
next: (response) => {
this.storageService.removeItem('partnerCode'); // Consume localStorage partnerCode
this.partnerCode = undefined;
this.partnerCodeService.clearStoredPartnerCode();
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
@ -883,8 +880,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.partnerCode
).subscribe({
next: (response) => {
this.storageService.removeItem('partnerCode'); // Consume localStorage partnerCode
this.partnerCode = undefined;
this.partnerCodeService.clearStoredPartnerCode();
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
@ -966,8 +962,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
bitcoinPaymentCompleted(): void {
this.storageService.removeItem('partnerCode'); // Consume localStorage partnerCode
this.partnerCode = undefined;
this.partnerCodeService.clearStoredPartnerCode();
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();

View file

@ -27,12 +27,12 @@ import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment
import { PriceService } from '@app/services/price.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { PartnerCodeService } from '@app/services/partner-code.service';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { TrackerStage } from '@components/tracker/tracker-bar.component';
import { MiningService, MiningStats } from '@app/services/mining.service';
import { ETA, EtaService } from '@app/services/eta.service';
import { getTransactionFlags, getUnacceleratedFeeRate } from '@app/shared/transaction.utils';
import { StorageService } from '@app/services/storage.service';
interface Pool {
@ -84,6 +84,8 @@ export class TrackerComponent implements OnInit, OnDestroy {
blocksSubscription: Subscription;
miningSubscription: Subscription;
currencyChangeSubscription: Subscription;
urlFragmentSubscription: Subscription;
partnerCodeSubscription: Subscription;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
latestReplacement: string;
@ -144,25 +146,20 @@ export class TrackerComponent implements OnInit, OnDestroy {
private seoService: SeoService,
private priceService: PriceService,
private enterpriseService: EnterpriseService,
private partnerCodeService: PartnerCodeService,
private miningService: MiningService,
private router: Router,
private cd: ChangeDetectorRef,
private zone: NgZone,
private storageService: StorageService,
@Inject(ZONE_SERVICE) private zoneService: any,
) {}
ngOnInit() {
this.onResize();
// Accelerator partner code in fragment
const fragmentsParams = new URLSearchParams(window.location.hash.slice(1));
this.partnerCode = fragmentsParams.get('partnerCode') ?? this.storageService.getValue('partnerCode') ?? undefined;
if (this.partnerCode) {
this.storageService.setValue('partnerCode', this.partnerCode);
// Cleanup partnerCode from url after storing it in localStorage to avoid re-use upon page reload
window.location.hash = window.location.hash.replace(`&partnerCode=${this.partnerCode}`, '').replace(`#partnerCode=${this.partnerCode}`, '');
}
this.setupPartnerCode();
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
this.consumePartnerCodeFragment(fragment);
});
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
@ -869,6 +866,36 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.miningSubscription?.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
this.enterpriseInfo$?.unsubscribe();
this.urlFragmentSubscription?.unsubscribe();
this.leaveTransaction();
this.partnerCodeSubscription?.unsubscribe();
}
setupPartnerCode(): void {
this.partnerCodeSubscription?.unsubscribe();
this.partnerCodeSubscription = this.partnerCodeService.partnerCode$.subscribe((partnerCode) => {
this.partnerCode = partnerCode;
this.cd.markForCheck();
});
}
// extract the partnerCode from the fragment string, pass to the partnerCodeService, and then remove from the URL
private consumePartnerCodeFragment(fragment: string | null): void {
const fragmentParams = new URLSearchParams(fragment || '');
if (!fragmentParams.has('partnerCode')) {
return;
}
const partnerCode = fragmentParams.get('partnerCode');
if (partnerCode) {
this.partnerCodeService.setFragmentPartnerCode(partnerCode);
}
fragmentParams.delete('partnerCode');
this.router.navigate([], {
relativeTo: this.route,
fragment: fragmentParams.toString() || null,
queryParamsHandling: 'merge',
replaceUrl: true,
});
}
}

View file

@ -15,7 +15,6 @@ import {
startWith,
repeat,
take,
debounceTime,
distinctUntilChanged
} from 'rxjs/operators';
import { Transaction } from '@interfaces/electrs.interface';
@ -37,6 +36,7 @@ import { PriceService } from '@app/services/price.service';
import { isFeatureActive } from '@app/bitcoin.utils';
import { ServicesApiServices } from '@app/services/services-api.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { PartnerCodeService } from '@app/services/partner-code.service';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { MiningService, MiningStats } from '@app/services/mining.service';
import { ETA, EtaService } from '@app/services/eta.service';
@ -104,6 +104,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
auditSubscription: Subscription;
txConfirmedSubscription: Subscription;
currencyChangeSubscription: Subscription;
partnerCodeSubscription: Subscription;
fragmentParams: URLSearchParams;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
@ -215,6 +216,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private priceService: PriceService,
private storageService: StorageService,
private enterpriseService: EnterpriseService,
private partnerCodeService: PartnerCodeService,
private miningService: MiningService,
private etaService: EtaService,
private cd: ChangeDetectorRef,
@ -222,6 +224,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
) {}
ngOnInit() {
this.setupPartnerCode();
this.enterpriseService.page();
this.isDetailsOpen = this.route.snapshot.queryParams['showDetails'] === 'true';
this.cpfpMode = this.route.snapshot.queryParams['cpfp'] === 'true';
@ -229,15 +233,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search);
this.forceAccelerationSummary = !!urlParams.get('cash_request_id');
// Accelerator partner code
const fragmentsParams = new URLSearchParams(window.location.hash.slice(1));
this.partnerCode = fragmentsParams.get('partnerCode') ?? this.storageService.getValue('partnerCode') ?? undefined;
if (this.partnerCode) {
this.storageService.setValue('partnerCode', this.partnerCode);
// Cleanup partnerCode from url after storing it in localStorage to avoid re-use upon page reload
window.location.hash = window.location.hash.replace(`&partnerCode=${this.partnerCode}`, '').replace(`#partnerCode=${this.partnerCode}`, '');
}
this.hideAccelerationSummary = this.stateService.isMempoolSpaceBuild ? this.storageService.getValue('hide-accelerator-pref') == 'true' : true;
if (!this.stateService.isLiquid()) {
@ -1145,7 +1140,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (anchor) {
params.set(anchor, '');
}
return Array.from(params.entries()).map(([key, value]) => `${key}=${value}`).join('&') || null;
return Array.from(params.entries()).map(([key, value]) => value ? `${key}=${value}` : key).join('&') || null;
}
toggleGraph() {
@ -1187,6 +1182,22 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
updateFragmentParams(fragment: string | null): void {
this.fragmentParams = new URLSearchParams(fragment || '');
// remove any partnerCode param from the URL and pass it to the partnerCodeService
if (this.fragmentParams.has('partnerCode')) {
const partnerCode = this.fragmentParams.get('partnerCode');
if (partnerCode) {
this.partnerCodeService.setFragmentPartnerCode(partnerCode);
}
this.fragmentParams.delete('partnerCode');
// update the URL with the new fragment string in a way Angular Router understands
const anchor = Array.from(this.fragmentParams.entries()).find(([, value]) => value === '')?.[0] || null;
this.router.navigate([], {
relativeTo: this.route,
fragment: this.formatFragment(this.fragmentParams, anchor),
queryParamsHandling: 'merge',
replaceUrl: true,
});
}
const vin = parseInt(this.fragmentParams.get('vin'), 10);
const vout = parseInt(this.fragmentParams.get('vout'), 10);
const inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
@ -1298,5 +1309,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.txConfirmedSubscription?.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();
this.leaveTransaction();
this.partnerCodeSubscription?.unsubscribe();
}
setupPartnerCode(): void {
this.partnerCodeSubscription?.unsubscribe();
this.partnerCodeSubscription = this.partnerCodeService.partnerCode$.subscribe((partnerCode) => {
this.partnerCode = partnerCode;
});
}
}

View file

@ -0,0 +1,52 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay } from 'rxjs/operators';
import { EnterpriseService } from '@app/services/enterprise.service';
import { StorageService } from '@app/services/storage.service';
@Injectable({
providedIn: 'root'
})
export class PartnerCodeService {
private readonly storedPartnerCode$: BehaviorSubject<string | undefined>;
readonly partnerCode$: Observable<string | undefined>;
constructor(
private enterpriseService: EnterpriseService,
private storageService: StorageService,
) {
// holds the most recent unused one-off code
const storedPartnerCode = this.storageService.getValue('partnerCode');
const isEnterprise = !!this.enterpriseService.getSubdomain();
this.storedPartnerCode$ = new BehaviorSubject<string | undefined>(storedPartnerCode || undefined);
const enterprisePartnerCode$ = this.enterpriseService.info$.pipe(
filter((info: object | null) => !isEnterprise || info !== null),
map((info: object | null) => (info as { name?: string } | null)?.name || undefined),
);
// enterprise partner code takes precedence, and remains applicable for all requests on this instance
// one-off code applies to the first successful request after being set, iff no enterprise code is present
this.partnerCode$ = combineLatest([
enterprisePartnerCode$,
this.storedPartnerCode$,
]).pipe(
map(([enterpriseCode, storedCode]) => enterpriseCode || storedCode),
distinctUntilChanged(),
shareReplay(1),
);
}
// set a one-off partner code from a URL fragment
setFragmentPartnerCode(partnerCode: string): void {
this.storageService.setValue('partnerCode', partnerCode);
this.storedPartnerCode$.next(partnerCode);
}
// clear any one-off code after successful use
clearStoredPartnerCode(): void {
this.storageService.removeItem('partnerCode');
this.storedPartnerCode$.next(undefined);
}
}