Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inBrowser } from '@clerk/shared/browser';

Check failure on line 1 in packages/clerk-js/src/core/resources/SignUp.ts

View workflow job for this annotation

GitHub Actions / Static analysis

Run autofix to sort these imports!
import { type ClerkError, ClerkRuntimeError, isCaptchaError, isClerkAPIResponseError } from '@clerk/shared/error';
import { PROTECT_CHECK_CONTAINER_ID } from '@clerk/shared/internal/clerk-js/constants';
import { createValidatePassword } from '@clerk/shared/internal/clerk-js/passwords/password';
import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
import { Poller } from '@clerk/shared/poller';
Expand Down Expand Up @@ -56,6 +57,7 @@
} from '../../utils/authenticateWithPopup';
import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
import { executeProtectCheck } from '../../utils/protectCheck';
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
import { loadZxcvbn } from '../../utils/zxcvbn';
import {
Expand Down Expand Up @@ -480,6 +482,36 @@
});
};

runProtectCheck = async (): Promise<SignUpResource> => {
// 1. Prepare — backend returns script URL in verifications.protect_check
await this._basePost({ action: 'prepare_protect_check' });

const scriptUrl = this.verifications.protectCheck?.url;
if (!scriptUrl) {
throw new ClerkRuntimeError('No protect check script URL returned', {
code: 'protect_check_missing_url',
});
}

// 2. Get or create container
const container = this.getOrCreateProtectCheckContainer();

try {
// 3. Load and execute script
const result = await executeProtectCheck(scriptUrl, this, container);

// 4. Attempt with result
await this._basePost({
body: result,
action: 'attempt_protect_check',
});
} finally {
this.cleanupProtectCheckContainer(container);
}

return this;
};

upsert = (params: SignUpCreateParams | SignUpUpdateParams): Promise<SignUpResource> => {
return this.id ? this.update(params) : this.create(params);
};
Expand Down Expand Up @@ -583,6 +615,25 @@
return false;
}

private getOrCreateProtectCheckContainer(): HTMLDivElement {
let el = document.getElementById(PROTECT_CHECK_CONTAINER_ID) as HTMLDivElement | null;
if (!el) {
el = document.createElement('div');
el.id = PROTECT_CHECK_CONTAINER_ID;
document.body.appendChild(el);
}
return el;
}

private cleanupProtectCheckContainer(el: HTMLDivElement) {
// Only remove from DOM if we created it (i.e., the UI didn't provide it)
if (el.parentNode && !document.getElementById(PROTECT_CHECK_CONTAINER_ID)) {
el.remove();
}
// Always clear inner content
el.innerHTML = '';
}

__experimental_getEnterpriseConnections = (): Promise<SignUpEnterpriseConnectionResource[]> => {
return BaseResource._fetch({
path: `/client/sign_ups/${this.id}/enterprise_connections`,
Expand All @@ -603,6 +654,7 @@
| 'waitForEmailLinkVerification'
| 'sendPhoneCode'
| 'verifyPhoneCode'
| 'runProtectCheck'
>;

class SignUpFutureVerifications implements SignUpFutureVerificationsType {
Expand All @@ -614,6 +666,7 @@
waitForEmailLinkVerification: SignUpFutureVerificationsType['waitForEmailLinkVerification'];
sendPhoneCode: SignUpFutureVerificationsType['sendPhoneCode'];
verifyPhoneCode: SignUpFutureVerificationsType['verifyPhoneCode'];
runProtectCheck: SignUpFutureVerificationsType['runProtectCheck'];

constructor(resource: SignUp, methods: SignUpFutureVerificationsMethods) {
this.#resource = resource;
Expand All @@ -623,6 +676,7 @@
this.waitForEmailLinkVerification = methods.waitForEmailLinkVerification;
this.sendPhoneCode = methods.sendPhoneCode;
this.verifyPhoneCode = methods.verifyPhoneCode;
this.runProtectCheck = methods.runProtectCheck;
}

get emailAddress() {
Expand All @@ -641,6 +695,10 @@
return this.#resource.verifications.externalAccount;
}

get protectCheck() {
return this.#resource.verifications.protectCheck;
}

get emailLinkVerification() {
if (!inBrowser()) {
return null;
Expand Down Expand Up @@ -681,6 +739,7 @@
waitForEmailLinkVerification: this.waitForEmailLinkVerification.bind(this),
sendPhoneCode: this.sendPhoneCode.bind(this),
verifyPhoneCode: this.verifyPhoneCode.bind(this),
runProtectCheck: this._runProtectCheck.bind(this),
});
}

Expand Down Expand Up @@ -832,6 +891,46 @@
return { captchaToken, captchaWidgetType, captchaError };
}

private async _runProtectCheck(): Promise<{ error: ClerkError | null }> {
return runAsyncResourceTask(this.#resource, async () => {
// 1. Prepare
await this.#resource.__internal_basePost({ action: 'prepare_protect_check' });

const scriptUrl = this.#resource.verifications.protectCheck?.url;
if (!scriptUrl) {
throw new ClerkRuntimeError('No protect check script URL returned', {
code: 'protect_check_missing_url',
});
}

// 2. Container
let container = document.getElementById(PROTECT_CHECK_CONTAINER_ID) as HTMLDivElement | null;
const createdContainer = !container;
if (!container) {
container = document.createElement('div');
container.id = PROTECT_CHECK_CONTAINER_ID;
document.body.appendChild(container);
}

try {
// 3. Execute script
const result = await executeProtectCheck(scriptUrl, this.#resource, container);

// 4. Attempt
await this.#resource.__internal_basePost({
body: result,
action: 'attempt_protect_check',
});
} finally {
if (createdContainer) {
container.remove();
} else {
container.innerHTML = '';
}
}
});
}

private async _create(params: SignUpFutureCreateParams): Promise<void> {
const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(params);

Expand Down
4 changes: 4 additions & 0 deletions packages/clerk-js/src/core/resources/Verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,21 @@ export class SignUpVerifications implements SignUpVerificationsResource {
phoneNumber: SignUpVerificationResource;
web3Wallet: SignUpVerificationResource;
externalAccount: VerificationResource;
protectCheck: { url: string } | null;

constructor(data: SignUpVerificationsJSON | SignUpVerificationsJSONSnapshot | null) {
if (data) {
this.emailAddress = new SignUpVerification(data.email_address);
this.phoneNumber = new SignUpVerification(data.phone_number);
this.web3Wallet = new SignUpVerification(data.web3_wallet);
this.externalAccount = new Verification(data.external_account);
this.protectCheck = data.protect_check ?? null;
} else {
this.emailAddress = new SignUpVerification(null);
this.phoneNumber = new SignUpVerification(null);
this.web3Wallet = new SignUpVerification(null);
this.externalAccount = new Verification(null);
this.protectCheck = null;
}
}

Expand All @@ -128,6 +131,7 @@ export class SignUpVerifications implements SignUpVerificationsResource {
phone_number: this.phoneNumber.__internal_toSnapshot(),
web3_wallet: this.web3Wallet.__internal_toSnapshot(),
external_account: this.externalAccount.__internal_toSnapshot(),
protect_check: this.protectCheck,
};
}
}
Expand Down
Loading
Loading