mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-23 05:36:12 +00:00
fix: nicer sms card fallback (#3621)
This commit is contained in:
@@ -29,6 +29,18 @@ import UIWindow from './UIWindow.js';
|
||||
// Stripe.js is loaded lazily from the CDN only when this dialog actually opens.
|
||||
// Used as a hard gate for low-reputation signups (after phone verification), so
|
||||
// by default it has no close button.
|
||||
//
|
||||
// `options.phone_fallback` opens the same dialog as the SMS escape hatch: a
|
||||
// user the phone gate keeps blocking verifies a card instead, which clears the
|
||||
// phone gate server-side (see /card-verification/confirm). Two things change in
|
||||
// that mode:
|
||||
// - The dialog can be dismissed back to phone verification, so choosing the
|
||||
// card path is never a one-way door.
|
||||
// - `card_verified` alone is NOT treated as success. The phone gate only
|
||||
// lifts when the server also reports `phone_verified`; anything else (the
|
||||
// kill switch short-circuit, in particular) would close a dialog that
|
||||
// reports success while the account is still phone-gated and every gated
|
||||
// route keeps 403ing. `options.on_unavailable` is called instead.
|
||||
|
||||
const STRIPE_JS_URL = 'https://js.stripe.com/v3/';
|
||||
|
||||
@@ -65,6 +77,7 @@ function UIWindowCardVerificationRequired(options) {
|
||||
'<svg style="width:24px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>';
|
||||
const verify_btn_txt = 'Verify Card';
|
||||
const retry_btn_txt = 'Try Again';
|
||||
const back_to_phone_txt = 'Verify by phone instead';
|
||||
|
||||
let h = '';
|
||||
if (options.show_close_button !== false) {
|
||||
@@ -83,8 +96,9 @@ function UIWindowCardVerificationRequired(options) {
|
||||
|
||||
// -- Card entry: Stripe Payment Element (hidden until setup succeeds) --
|
||||
h += '<form class="card-step card-step-form" style="display:none;">';
|
||||
h +=
|
||||
'<p style="text-align:center; padding: 0 20px; font-size:13px;">Verify a card to continue. You will <b><i>not</i></b> be charged.</p>';
|
||||
h += options.phone_fallback
|
||||
? '<p style="text-align:center; padding: 0 20px; font-size:13px;">Verify a card instead of your phone number. You will <b><i>not</i></b> be charged.</p>'
|
||||
: '<p style="text-align:center; padding: 0 20px; font-size:13px;">Verify a card to continue. You will <b><i>not</i></b> be charged.</p>';
|
||||
// Offer a friendly human fallback so verification is never a dead end —
|
||||
// worded as help, not as an accusation.
|
||||
h += `<p style="text-align:center; font-size:12px; line-height:1.4; color:#8a99a8; margin:6px auto 14px; max-width:320px;">Need help? Email <a href="mailto:support@puter.com" style="color:#3b82f6; text-decoration:none;">support@puter.com</a> and we'll assist you creating your account.</p>`;
|
||||
@@ -98,7 +112,13 @@ function UIWindowCardVerificationRequired(options) {
|
||||
h +=
|
||||
'<p style="text-align:center; padding: 0 20px;">Card verification is temporarily unavailable. Please try again in a few minutes.</p>';
|
||||
h += `<button type="button" class="button button-block button-primary card-retry-btn" style="margin-top:10px;">${retry_btn_txt}</button>`;
|
||||
if (!options.logout_in_footer) {
|
||||
if (options.phone_fallback) {
|
||||
h +=
|
||||
'<div style="text-align:center; padding:10px; font-size:14px; margin-top:10px;">';
|
||||
h += `<span class="card-back-to-phone" style="cursor:pointer; color:#3b82f6;">${back_to_phone_txt}</span>`;
|
||||
h += '</div>';
|
||||
}
|
||||
if (!options.logout_in_footer && !options.phone_fallback) {
|
||||
h +=
|
||||
'<div style="text-align:center; padding:10px; font-size:14px; margin-top:10px;">';
|
||||
h += `<span class="card-log-out" style="cursor:pointer;">${i18n('log_out')}</span>`;
|
||||
@@ -106,7 +126,16 @@ function UIWindowCardVerificationRequired(options) {
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
if (options.logout_in_footer) {
|
||||
// In fallback mode the footer offers the way back to the phone gate
|
||||
// rather than a log-out: the user still has a working path to an
|
||||
// account, and dropping them out of the flow entirely is the outcome
|
||||
// this mode exists to avoid.
|
||||
if (options.phone_fallback) {
|
||||
h +=
|
||||
'<div style="text-align:center; padding:14px 10px 4px; margin-top:6px; border-top:1px solid #e9eef3; font-size:14px;">';
|
||||
h += `<span class="card-back-to-phone" style="cursor:pointer; color:#3b82f6;">${back_to_phone_txt}</span>`;
|
||||
h += '</div>';
|
||||
} else if (options.logout_in_footer) {
|
||||
h +=
|
||||
'<div style="text-align:center; padding:14px 10px 4px; margin-top:6px; border-top:1px solid #e9eef3; font-size:14px;">';
|
||||
h += `<span class="card-log-out" style="cursor:pointer;">${i18n('log_out')}</span>`;
|
||||
@@ -170,12 +199,38 @@ function UIWindowCardVerificationRequired(options) {
|
||||
$(el_window).find(`.card-step-${name}`).show();
|
||||
};
|
||||
|
||||
// Resolve at most once: `finish` and `backToPhone` are the only exits
|
||||
// and a double close would operate on a detached window.
|
||||
let settled = false;
|
||||
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
$(el_window).close();
|
||||
window.refresh_user_data(window.auth_token);
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
// Fallback mode only: leave without clearing the gate. Resolving false
|
||||
// hands the user back to whoever opened this dialog (the phone
|
||||
// verification window), which is still the live gate on the account.
|
||||
const backToPhone = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
$(el_window).close();
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
// Fallback mode only: the server answered `card_verified` without
|
||||
// lifting the phone gate — the card path can't clear this account
|
||||
// (typically the server-side kill switch). Reporting success here would
|
||||
// close the dialog on a still-phone-gated account, so bounce back to
|
||||
// the phone gate with the reason instead.
|
||||
const cardCannotClearPhoneGate = () => {
|
||||
options.on_unavailable?.();
|
||||
backToPhone();
|
||||
};
|
||||
|
||||
const mountPaymentElement = async (publishable_key, client_secret) => {
|
||||
await loadStripeJs();
|
||||
stripe = window.Stripe(publishable_key);
|
||||
@@ -220,6 +275,13 @@ function UIWindowCardVerificationRequired(options) {
|
||||
// Already verified, or the feature was disabled server-side
|
||||
// (kill switch) — either way the gate is satisfied.
|
||||
if (res.card_verified) {
|
||||
// ...except in fallback mode, where the phone gate is
|
||||
// the one that has to lift and this short-circuit never
|
||||
// lifts it.
|
||||
if (options.phone_fallback) {
|
||||
cardCannotClearPhoneGate();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@@ -321,6 +383,13 @@ function UIWindowCardVerificationRequired(options) {
|
||||
statusCode: { 401: (xhr) => window.handle401(xhr) },
|
||||
success: function (res) {
|
||||
if (res.card_verified) {
|
||||
// In fallback mode the card is only worth anything
|
||||
// if it took the phone gate down with it; the
|
||||
// server says so with `phone_verified`.
|
||||
if (options.phone_fallback && !res.phone_verified) {
|
||||
cardCannotClearPhoneGate();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
@@ -365,6 +434,12 @@ function UIWindowCardVerificationRequired(options) {
|
||||
$(el_window).close();
|
||||
});
|
||||
|
||||
$(el_window)
|
||||
.find('.card-back-to-phone')
|
||||
.on('click', function () {
|
||||
backToPhone();
|
||||
});
|
||||
|
||||
startSetup();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import UIWindow from './UIWindow.js';
|
||||
import UIWindowCardVerificationRequired from './UIWindowCardVerificationRequired.js';
|
||||
import { get_country_list } from '../helpers/country_codes.js';
|
||||
import {
|
||||
format_phone_as_you_type,
|
||||
@@ -32,6 +33,13 @@ import {
|
||||
// The 6-digit code UX mirrors UIWindowEmailConfirmationRequired.js. Used as a
|
||||
// hard gate for low-reputation signups, so by default it has no close button.
|
||||
//
|
||||
// When the server reports `card_fallback_available` on a send (either a
|
||||
// successful one or a refusal), SMS is not the only way out: the backend has
|
||||
// opened a card-verification path that clears the phone gate too. This dialog
|
||||
// surfaces that as an opt-in link rather than leaving the user to retry a send
|
||||
// that keeps failing. The card dialog can be dismissed straight back here, so
|
||||
// the choice is reversible either way.
|
||||
//
|
||||
// The number field combines a searchable country-code picker with the national
|
||||
// number. Everything the user types is normalized to E.164 with libphonenumber
|
||||
// before it's sent, so country selection and on-screen formatting are purely a
|
||||
@@ -65,8 +73,13 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
let is_checking_code = false;
|
||||
let is_sending = false;
|
||||
// Resolve the returned promise at most once. Success resolves(true);
|
||||
// a user-initiated close resolves(false). Idempotent so the close hook
|
||||
// can fire after a success without clobbering the result.
|
||||
// a user-initiated close resolves(false). The card fallback resolves
|
||||
// the string 'card' — truthy, so callers polling `while (!ok)` are
|
||||
// unaffected, but distinguishable for the one caller decision it
|
||||
// changes: a fallback card clears the card gate along with the phone
|
||||
// gate, so the card dialog must not be opened again afterwards.
|
||||
// Idempotent so the close hook can fire after a success without
|
||||
// clobbering the result.
|
||||
let settled = false;
|
||||
const settle = (val) => {
|
||||
if (settled) return;
|
||||
@@ -113,6 +126,10 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
select_country: i18n('phone_select_country'),
|
||||
code_sent_to: i18n('phone_code_sent_to'),
|
||||
code_sent_whatsapp: i18n('phone_code_sent_whatsapp'),
|
||||
card_fallback_prompt: i18n('phone_card_fallback_prompt'),
|
||||
card_fallback_link: i18n('phone_card_fallback_link'),
|
||||
card_fallback_note: i18n('phone_card_fallback_note'),
|
||||
card_fallback_unavailable: i18n('phone_card_fallback_unavailable'),
|
||||
suggested: i18n('phone_suggested'),
|
||||
all_countries: i18n('phone_all_countries'),
|
||||
};
|
||||
@@ -195,6 +212,15 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
.map((c) => renderOption(c, 'cc-opt-', 'all'))
|
||||
.join('');
|
||||
|
||||
// Card escape hatch, rendered into both steps and revealed only when a
|
||||
// send response says the backend opened it.
|
||||
const card_fallback_html =
|
||||
'<div class="phone-card-fallback" hidden>' +
|
||||
`<p class="phone-card-fallback-prompt">${T.card_fallback_prompt}</p>` +
|
||||
`<a class="phone-use-card" role="button" tabindex="0">${T.card_fallback_link}</a>` +
|
||||
`<p class="phone-card-fallback-note">${T.card_fallback_note}</p>` +
|
||||
'</div>';
|
||||
|
||||
let h = '';
|
||||
// Scoped styling for this dialog.
|
||||
h += `<style>
|
||||
@@ -333,6 +359,25 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
.window-confirm-phone-using-code .error {
|
||||
color: #c0392b; font-size: 13px; text-align: center; margin-bottom: 10px;
|
||||
}
|
||||
/* Card escape hatch — hidden until the server says the fallback is
|
||||
open, so a working SMS flow never advertises it. */
|
||||
.window-confirm-phone-using-code .phone-card-fallback {
|
||||
text-align: center; margin-top: 16px; padding-top: 14px;
|
||||
border-top: 1px solid #e9eef3;
|
||||
}
|
||||
.window-confirm-phone-using-code .phone-card-fallback[hidden] { display: none; }
|
||||
.window-confirm-phone-using-code .phone-card-fallback-prompt {
|
||||
font-size: 13px; color: #6b7c8c; margin: 0 0 4px;
|
||||
}
|
||||
.window-confirm-phone-using-code .phone-use-card {
|
||||
display: inline-block; font-size: 14px; font-weight: 500;
|
||||
color: #3b82f6; cursor: pointer; text-decoration: none;
|
||||
}
|
||||
.window-confirm-phone-using-code .phone-use-card:hover { text-decoration: underline; }
|
||||
.window-confirm-phone-using-code .phone-card-fallback-note {
|
||||
font-size: 12px; line-height: 1.4; color: #8a99a8;
|
||||
margin: 6px auto 0; max-width: 300px;
|
||||
}
|
||||
/* Touch devices: meet the ~44px minimum tap target for list rows
|
||||
and the search field; keep the input >=16px to avoid iOS focus-zoom. */
|
||||
@media (pointer: coarse) {
|
||||
@@ -378,6 +423,7 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
h += `<div class="phone-country-empty" hidden>${T.no_matches}</div>`;
|
||||
h += '</div>';
|
||||
h += `<button type="submit" class="button button-block button-primary phone-send-btn">${send_btn_txt}</button>`;
|
||||
h += card_fallback_html;
|
||||
if (options.logout_in_footer) {
|
||||
h += `<div class="phone-footer"><a class="phone-log-out">${i18n('log_out')}</a></div>`;
|
||||
}
|
||||
@@ -399,6 +445,7 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
<input class="digit-input" type="number" min='0' max='9' inputmode="numeric" name='number-code-5' data-number-code-input='5' required />
|
||||
</fieldset>`;
|
||||
h += `<button type="submit" class="button button-block button-primary phone-verify-btn" disabled>${verify_btn_txt}</button>`;
|
||||
h += card_fallback_html;
|
||||
h += '<div class="phone-footer">';
|
||||
h += `<a class="phone-resend-code">${T.resend_code}</a> • <a class="phone-change-number">${T.change_number}</a>`;
|
||||
if (options.logout_in_footer) {
|
||||
@@ -463,6 +510,79 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
$(el_window).find('.error').hide();
|
||||
};
|
||||
|
||||
// ---------- Card escape hatch ----------
|
||||
//
|
||||
// Revealed by a send response carrying `card_fallback_available`. Once
|
||||
// revealed it stays: the backend keeps the eligibility open for hours,
|
||||
// and a user who came back to try SMS again shouldn't lose the way out
|
||||
// they were already offered.
|
||||
let card_fallback_available = false;
|
||||
let card_fallback_in_progress = false;
|
||||
const revealCardFallback = () => {
|
||||
if (card_fallback_available) return;
|
||||
card_fallback_available = true;
|
||||
$(el_window).find('.phone-card-fallback').prop('hidden', false);
|
||||
};
|
||||
|
||||
// Hand off to the card dialog. This window stays alive behind it (just
|
||||
// hidden) so a user who backs out — or whose card path turns out to be
|
||||
// unavailable server-side — lands back on the live gate instead of on
|
||||
// an empty desktop that 403s every request.
|
||||
const useCardInstead = async () => {
|
||||
if (card_fallback_in_progress) return;
|
||||
card_fallback_in_progress = true;
|
||||
clearError();
|
||||
let unavailable = false;
|
||||
// Hiding the backdrop too, so the card dialog's own backdrop is the
|
||||
// only one on screen.
|
||||
const $hidden = $(el_window).closest('.window-backdrop').length
|
||||
? $(el_window).closest('.window-backdrop')
|
||||
: $(el_window);
|
||||
$hidden.hide();
|
||||
let verified = false;
|
||||
try {
|
||||
verified = await UIWindowCardVerificationRequired({
|
||||
phone_fallback: true,
|
||||
on_unavailable: () => {
|
||||
unavailable = true;
|
||||
},
|
||||
show_close_button: false,
|
||||
stay_on_top: options.stay_on_top ?? false,
|
||||
is_draggable: options.is_draggable,
|
||||
window_options: options.window_options,
|
||||
});
|
||||
} catch (e) {
|
||||
console.debug('Card verification dialog failed:', e);
|
||||
}
|
||||
if (verified) {
|
||||
// The card cleared the phone gate server-side, so this gate is
|
||||
// satisfied — settle before closing so the on_close hook's
|
||||
// resolve(false) is the no-op. 'card' rather than true so the
|
||||
// caller knows the card gate went down with it.
|
||||
settle('card');
|
||||
$(el_window).close();
|
||||
return;
|
||||
}
|
||||
card_fallback_in_progress = false;
|
||||
// A logout (or a parent cascade) took this window with it; nothing
|
||||
// left to restore.
|
||||
if (!document.body.contains(el_window)) return;
|
||||
$hidden.show();
|
||||
if (unavailable) showError(T.card_fallback_unavailable);
|
||||
};
|
||||
|
||||
$(el_window)
|
||||
.find('.phone-use-card')
|
||||
.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
useCardInstead();
|
||||
})
|
||||
.on('keydown', function (e) {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
e.preventDefault();
|
||||
useCardInstead();
|
||||
});
|
||||
|
||||
// 6-digit code DOM refs (used by paste/WebOTP and the per-digit logic).
|
||||
const numberCodeForm = el_window.querySelector('[data-number-code-form]');
|
||||
const numberCodeInputs = [
|
||||
@@ -749,6 +869,10 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
headers: { Authorization: `Bearer ${window.auth_token}` },
|
||||
statusCode: { 401: (xhr) => window.handle401(xhr) },
|
||||
success: function (res) {
|
||||
// The backend counts send *attempts*, so the fallback can
|
||||
// open on a send that itself succeeded — the user still may
|
||||
// never receive the code.
|
||||
if (res?.card_fallback_available) revealCardFallback();
|
||||
// Advance to the code-entry step with a clean slate.
|
||||
$(el_window).find('.phone-target').text(phone);
|
||||
// `channel` is where Prelude actually delivered the code;
|
||||
@@ -776,6 +900,8 @@ function UIWindowPhoneVerificationRequired(options) {
|
||||
startWebOTP();
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.responseJSON?.card_fallback_available)
|
||||
revealCardFallback();
|
||||
const reason = xhr.responseJSON?.reason;
|
||||
let msg =
|
||||
SEND_REASON_MESSAGES[reason] ??
|
||||
|
||||
@@ -540,6 +540,11 @@ function UIWindowSignup(options) {
|
||||
options.window_options ?? {},
|
||||
});
|
||||
}
|
||||
// A user the SMS path keeps failing may take the
|
||||
// card fallback instead; that resolves 'card' and
|
||||
// clears the card gate along with the phone gate
|
||||
// (see UIWindowPhoneVerificationRequired).
|
||||
let card_gate_cleared_by_fallback = false;
|
||||
if (data.user?.requires_phone_verification) {
|
||||
let phone_ok = false;
|
||||
do {
|
||||
@@ -555,9 +560,14 @@ function UIWindowSignup(options) {
|
||||
},
|
||||
);
|
||||
} while (!phone_ok);
|
||||
card_gate_cleared_by_fallback =
|
||||
phone_ok === 'card';
|
||||
}
|
||||
// Card verification is the last gate.
|
||||
if (data.user?.requires_card_verification) {
|
||||
if (
|
||||
data.user?.requires_card_verification &&
|
||||
!card_gate_cleared_by_fallback
|
||||
) {
|
||||
let card_ok = false;
|
||||
do {
|
||||
card_ok =
|
||||
|
||||
@@ -282,6 +282,10 @@ const en = {
|
||||
phone_select_country: 'Select country',
|
||||
phone_code_sent_to: 'Enter the 6-digit code sent to',
|
||||
phone_code_sent_whatsapp: 'Enter the 6-digit code sent via WhatsApp to',
|
||||
phone_card_fallback_prompt: "Can't receive a code?",
|
||||
phone_card_fallback_link: 'Verify with a card instead',
|
||||
phone_card_fallback_note: "We only check that the card is valid \u2014 you won't be charged.",
|
||||
phone_card_fallback_unavailable: "Card verification isn't available right now. Please try your phone number again, or email support@puter.com and we'll help you finish setting up your account.",
|
||||
pick_name_for_website: 'Pick a name for your website:',
|
||||
pick_name_for_worker: 'Pick a name for your worker:',
|
||||
picture: 'Picture',
|
||||
|
||||
+18
-2
@@ -1684,6 +1684,10 @@ window.initgui = async function (options) {
|
||||
} while (!is_verified);
|
||||
}
|
||||
// is phone verification required? (hard gate for low-rep signups)
|
||||
// A user the SMS path keeps failing may be offered the card
|
||||
// fallback instead; that resolves 'card' and clears the card gate
|
||||
// too (see UIWindowPhoneVerificationRequired).
|
||||
let card_gate_cleared_by_fallback = false;
|
||||
if (whoami.requires_phone_verification) {
|
||||
let is_verified;
|
||||
do {
|
||||
@@ -1696,10 +1700,14 @@ window.initgui = async function (options) {
|
||||
},
|
||||
});
|
||||
} while (!is_verified);
|
||||
card_gate_cleared_by_fallback = is_verified === 'card';
|
||||
}
|
||||
// Card verification is the last gate: only show it once the email and
|
||||
// phone (SMS) gates are cleared, since those show up first.
|
||||
if (whoami.requires_card_verification) {
|
||||
if (
|
||||
whoami.requires_card_verification &&
|
||||
!card_gate_cleared_by_fallback
|
||||
) {
|
||||
let is_verified;
|
||||
do {
|
||||
is_verified = await UIWindowCardVerificationRequired({
|
||||
@@ -1948,6 +1956,10 @@ window.initgui = async function (options) {
|
||||
} while (!is_verified);
|
||||
}
|
||||
// is phone verification required? (hard gate for low-rep signups)
|
||||
// A user the SMS path keeps failing may be offered the card
|
||||
// fallback instead; that resolves 'card' and clears the card gate
|
||||
// too (see UIWindowPhoneVerificationRequired).
|
||||
let card_gate_cleared_by_fallback = false;
|
||||
if (whoami.requires_phone_verification) {
|
||||
let is_verified;
|
||||
do {
|
||||
@@ -1962,10 +1974,14 @@ window.initgui = async function (options) {
|
||||
},
|
||||
});
|
||||
} while (!is_verified);
|
||||
card_gate_cleared_by_fallback = is_verified === 'card';
|
||||
}
|
||||
// Card verification is the last gate: only show it once the email and
|
||||
// phone (SMS) gates are cleared, since those show up first.
|
||||
if (whoami.requires_card_verification) {
|
||||
if (
|
||||
whoami.requires_card_verification &&
|
||||
!card_gate_cleared_by_fallback
|
||||
) {
|
||||
let is_verified;
|
||||
do {
|
||||
is_verified = await UIWindowCardVerificationRequired({
|
||||
|
||||
Reference in New Issue
Block a user