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:
@@ -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',
|
||||||
|
|||||||
@@ -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-ով',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user