fix: cart email/phone capture form was never rendered

recordOrder() and autoSubmitPurchase() read userEmail()/userPhone()
signals and submitEmail() was fully implemented (validation, error
handling), but the success screen's template never rendered the
inputs - so the form was unreachable and the fallback auto-submit
always sent blank email/phone.

- Added the email/phone form to the payment-success screen, wired to
  the existing signals/handlers.
- autoSubmitPurchase() (the 5s fallback if the user doesn't submit
  manually) no longer navigates home via an unconditional setTimeout(0)
  fired before the submission result is known - it now waits for
  submitPurchaseEmail() to settle, same as the manual path, and skips
  entirely if the user already submitted (new purchaseSubmitted flag).
- It also now sends whatever the user has typed instead of
  hardcoded-blank fields, and shows a toast instead of only logging to
  console when no Telegram user id is available.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 07:37:36 +04:00
parent ebca66dd4c
commit 4c4417dc1d
7 changed files with 134 additions and 17 deletions

View File

@@ -123,6 +123,9 @@ export const en: Translations = {
emailNeedsAt: 'Email must contain @', emailNeedsAt: 'Email must contain @',
emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)', emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)',
emailInvalid: 'Invalid email format', emailInvalid: 'Invalid email format',
telegramIdMissing: 'We could not identify your Telegram account, so we could not save your contact details. Your payment was still successful.',
emailPlaceholder: 'you@example.com',
phonePlaceholder: '+7 (___) ___-__-__',
loginRequired: 'Log in to checkout', loginRequired: 'Log in to checkout',
loginRequiredDesc: 'Please log in via Telegram to place your order', loginRequiredDesc: 'Please log in via Telegram to place your order',
loginWithTelegram: 'Log in with Telegram', loginWithTelegram: 'Log in with Telegram',

View File

@@ -123,6 +123,9 @@ export const hy: Translations = {
emailNeedsAt: 'Email-ը պետք է պարունակի @', emailNeedsAt: 'Email-ը պետք է պարունակի @',
emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեյն (.com, .ru և այլն)', emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեյն (.com, .ru և այլն)',
emailInvalid: 'Սխալ email ձևաչափ', emailInvalid: 'Սխալ email ձևաչափ',
telegramIdMissing: 'Չհաջողվեց հաստատել ձեր Telegram հաշիվը, ուստի կոնտակտային տվյալները չեն պահպանվել։ Վճարումը հաջողությամբ կատարվել է։',
emailPlaceholder: 'you@example.com',
phonePlaceholder: '+7 (___) ___-__-__',
loginRequired: 'Մուտք գործեք ձևակերպելու համար', loginRequired: 'Մուտք գործեք ձևակերպելու համար',
loginRequiredDesc: 'Պատվեր ձևակերպելու համար մուտք գործեք Telegram-ով', loginRequiredDesc: 'Պատվեր ձևակերպելու համար մուտք գործեք Telegram-ով',
loginWithTelegram: 'Մուտք Telegram-ով', loginWithTelegram: 'Մուտք Telegram-ով',

View File

@@ -123,6 +123,9 @@ export const ru: Translations = {
emailNeedsAt: 'Email должен содержать @', emailNeedsAt: 'Email должен содержать @',
emailNeedsDomain: 'Email должен содержать домен (.com, .ru и т.д.)', emailNeedsDomain: 'Email должен содержать домен (.com, .ru и т.д.)',
emailInvalid: 'Некорректный формат email', emailInvalid: 'Некорректный формат email',
telegramIdMissing: 'Не удалось определить ваш Telegram-аккаунт, поэтому контактные данные не сохранены. Оплата прошла успешно.',
emailPlaceholder: 'you@example.com',
phonePlaceholder: '+7 (___) ___-__-__',
loginRequired: 'Войдите для оформления', loginRequired: 'Войдите для оформления',
loginRequiredDesc: 'Для оформления заказа войдите через Telegram', loginRequiredDesc: 'Для оформления заказа войдите через Telegram',
loginWithTelegram: 'Войти через Telegram', loginWithTelegram: 'Войти через Telegram',

View File

@@ -121,6 +121,9 @@ export interface Translations {
emailNeedsAt: string; emailNeedsAt: string;
emailNeedsDomain: string; emailNeedsDomain: string;
emailInvalid: string; emailInvalid: string;
telegramIdMissing: string;
emailPlaceholder: string;
phonePlaceholder: string;
loginRequired: string; loginRequired: string;
loginRequiredDesc: string; loginRequiredDesc: string;
loginWithTelegram: string; loginWithTelegram: string;

View File

@@ -271,6 +271,44 @@
<div class="payment-status-screen success" role="status" aria-live="polite"> <div class="payment-status-screen success" role="status" aria-live="polite">
<div class="success-icon" aria-hidden="true"></div> <div class="success-icon" aria-hidden="true"></div>
<h2>{{ 'cart.paymentSuccess' | translate }}</h2> <h2>{{ 'cart.paymentSuccess' | translate }}</h2>
@if (!purchaseSubmitted()) {
<p>{{ 'cart.paymentSuccessDesc' | translate }}</p>
<form class="contact-capture-form" (ngSubmit)="submitEmail()">
<label>
<input
type="email"
name="email"
[value]="userEmail()"
[placeholder]="'cart.emailPlaceholder' | translate"
[attr.aria-invalid]="!!emailError()"
(input)="onEmailInput($event)"
(blur)="onEmailBlur()"
[disabled]="emailSubmitting()"
/>
@if (emailError()) {
<span class="field-error">{{ emailError() }}</span>
}
</label>
<label>
<input
type="tel"
name="phone"
[value]="userPhone()"
[placeholder]="'cart.phonePlaceholder' | translate"
[attr.aria-invalid]="!!phoneError()"
(input)="onPhoneInput($event)"
(blur)="onPhoneBlur()"
[disabled]="emailSubmitting()"
/>
@if (phoneError()) {
<span class="field-error">{{ phoneError() }}</span>
}
</label>
<button type="submit" [disabled]="emailSubmitting()">
{{ emailSubmitting() ? ('cart.sending' | translate) : ('cart.send' | translate) }}
</button>
</form>
}
</div> </div>
} }

View File

@@ -722,6 +722,57 @@
color: white; color: white;
margin: 0 auto 20px; margin: 0 auto 20px;
} }
.contact-capture-form {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
text-align: left;
label {
display: flex;
flex-direction: column;
gap: 4px;
}
input {
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md, 8px);
font-size: var(--font-size-sm, 0.875rem);
color: var(--text-primary);
background: var(--surface-color, #fff);
&[aria-invalid='true'] {
border-color: var(--error-color);
}
&:disabled {
opacity: 0.6;
}
}
.field-error {
font-size: var(--font-size-xs, 0.75rem);
color: var(--error-color);
}
button {
padding: 10px 16px;
border: none;
border-radius: var(--radius-md, 8px);
background: var(--primary-color, #2563eb);
color: white;
font-weight: var(--font-weight-bold, 700);
cursor: pointer;
&:disabled {
opacity: 0.6;
cursor: default;
}
}
}
} }
&.error { &.error {

View File

@@ -71,6 +71,7 @@ export class CartComponent implements OnDestroy {
emailError = signal<string>(''); emailError = signal<string>('');
phoneError = signal<string>(''); phoneError = signal<string>('');
emailSubmitting = signal<boolean>(false); emailSubmitting = signal<boolean>(false);
purchaseSubmitted = signal<boolean>(false);
paidItems: CartItem[] = []; paidItems: CartItem[] = [];
maxChecks = Math.ceil(PAYMENT_MIN_POLL_SECONDS / (PAYMENT_POLL_INTERVAL_MS / 1000)); maxChecks = Math.ceil(PAYMENT_MIN_POLL_SECONDS / (PAYMENT_POLL_INTERVAL_MS / 1000));
@@ -219,6 +220,7 @@ export class CartComponent implements OnDestroy {
this.emailError.set(''); this.emailError.set('');
this.phoneError.set(''); this.phoneError.set('');
this.emailSubmitting.set(false); this.emailSubmitting.set(false);
this.purchaseSubmitted.set(false);
this.paidItems = [...this.items()]; this.paidItems = [...this.items()];
this.createPayment(paymentMethod); this.createPayment(paymentMethod);
} }
@@ -439,29 +441,39 @@ export class CartComponent implements OnDestroy {
}); });
} }
/**
* Fallback fired a few seconds after payment success if the user hasn't
* already submitted the email/phone form themselves (submitEmail()).
* Navigates home only once the submission result is known, never before -
* and sends whatever the user has typed so far instead of blank fields.
*/
private autoSubmitPurchase(): void { private autoSubmitPurchase(): void {
setTimeout(() => { if (this.purchaseSubmitted()) {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);}, 0);
const telegramUserId = this.getTelegramUserId();
// Telegram ID is mandatory
if (!telegramUserId) {
console.error('Cannot submit purchase: Telegram ID is required');
this.emailSubmitting.set(false);
return; return;
} }
const telegramUserId = this.getTelegramUserId();
// Telegram ID is mandatory for submitPurchaseEmail.
if (!telegramUserId) {
this.notifications.show(this.i18n.t('cart.telegramIdMissing'), 'warning');
this.emailSubmitting.set(false);
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
return;
}
this.emailSubmitting.set(true); this.emailSubmitting.set(true);
const emailData = { const emailData = {
email: '', email: this.userEmail().trim(),
phone: '', phone: this.userPhone().replace(/\D/g, ''),
telegramUserId: telegramUserId, telegramUserId: telegramUserId,
items: this.paidItems.map((item: CartItem) => ({ items: this.paidItems.map((item: CartItem) => ({
itemID: item.itemID, itemID: item.itemID,
name: item.name, name: item.name,
price: item.discount > 0 price: item.discount > 0
? item.price * (1 - item.discount / 100) ? item.price * (1 - item.discount / 100)
: item.price, : item.price,
currency: item.currency, currency: item.currency,
@@ -469,9 +481,10 @@ export class CartComponent implements OnDestroy {
...(item.selectedDelivery ? { delivery: [item.selectedDelivery] } : {}) ...(item.selectedDelivery ? { delivery: [item.selectedDelivery] } : {})
})) }))
}; };
this.apiService.submitPurchaseEmail(emailData).subscribe({ this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => { next: () => {
this.purchaseSubmitted.set(true);
this.emailSubmitting.set(false); this.emailSubmitting.set(false);
this.closePaymentPopup(); this.closePaymentPopup();
const lang = this.langService.currentLanguage(); const lang = this.langService.currentLanguage();
@@ -487,8 +500,6 @@ export class CartComponent implements OnDestroy {
} }
}); });
this.paymentStatus.set(null); this.paymentStatus.set(null);
} }
copyPaymentLink(): void { copyPaymentLink(): void {
@@ -541,6 +552,11 @@ export class CartComponent implements OnDestroy {
this.apiService.submitPurchaseEmail(emailData).subscribe({ this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => { next: () => {
this.purchaseSubmitted.set(true);
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
}
this.emailSubmitting.set(false); this.emailSubmitting.set(false);
this.notifications.show(this.i18n.t('cart.emailSuccess'), 'success'); this.notifications.show(this.i18n.t('cart.emailSuccess'), 'success');
// Close popup and redirect to home page // Close popup and redirect to home page