Hi I am trying to integrate the Bring your own scr...
# ask-a-descoper
a
Hi I am trying to integrate the Bring your own screen with https://docs.descope.com/getting-started/oidc-endpoints#using-pkce-in-your-endpoints but i have question regarding the byos as my flow starts from signup or sign in screen whose screen name is Login and then it goes to my Create Account screen so how shalli define my byos component to work with it so that it directly goes to Create account screen. I have taken ref from byos example in gitlab this is my component created
Copy code
import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { environment } from '../../environments/environment';
import { CustomWelcomeScreenComponent } from '../custom-welcome-screen/custom-welcome-screen.component';
import { DescopeAuthModule } from '@descope/angular-sdk';

@Component({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  selector: 'app-byos-demo',
  templateUrl: './test.html',
  styleUrls: ['./test.scss'],
  standalone: true,
  imports: [CommonModule, CustomWelcomeScreenComponent, DescopeAuthModule]
})
export class ByosDemoComponent implements OnInit {
  readonly flowId: string = environment.descopeFlowId || 'passwords-with-explicit-sign-up-byos';

  state: {
    error: { text: string; type: string } | null;
    screenName?: string;
    next?: (interactionId: string, form: Record<string, unknown>) => Promise<unknown>;
  } = {
    error: null,
    screenName: undefined,
    next: undefined
  };

  form: { name: string; email: string } = { name: '', email: '' };

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private cdr: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    // If Descope redirected back here with ?code=, forward to exchange-token
    const code = this.route.snapshot.queryParamMap.get('code');
    const state = this.route.snapshot.queryParamMap.get('state');
    // if (code) {
    //   this.router.navigate(['/exchange-token'], {
    //     queryParams: { code, state }
    //   });
    // }
  }

  onScreenUpdate = (
    screenName: string,
    context: Record<string, unknown>,
    next: (interactionId: string, form: Record<string, unknown>) => Promise<unknown>
  ): boolean => {
    this.state = {
      ...this.state,
      ...context,
      screenName,
      next
    };
    console.log('Screen update:', screenName, this.state);
    this.cdr.detectChanges();
    return screenName === 'Login';
  };

  onSuccess(event: CustomEvent): void {
    console.log('Authentication successful:', event.detail);
    // After BYOS flow completes successfully, navigate to exchange-token
    // this.router.navigate(['/exchange-token']);
  }

  onError(event: CustomEvent): void {
    console.error('Authentication error:', event);
  }

  updateForm(formData: { name: string; email: string }): void {
    this.form = { ...formData };
  }

  async continueFlow(): Promise<void> {
    try {
      await this.state.next?.('XLmMGCy6IS', { ...this.form });
    } catch (error) {
      console.error('Error continuing flow:', error);
    }
  }
}
this is my byos html
Copy code
<div class="byos-container">
  <h2>Custom Authentication Example</h2>
  <p>
    This example demonstrates how to use custom screens with Descope
    authentication
  </p>
  <div class="auth-container">
    <descope
      [flowId]="flowId"
      [onScreenUpdate]="onScreenUpdate"
      (success)="onSuccess($event)"
      (error)="onError($event)"
    >
      <!-- 
        This slot content is only shown when onScreenUpdate returns true.
        Descope will pause the flow and wait until next() is called explicitly
        via continueFlow(), which only happens on button click.
      -->
      <app-custom-welcome-screen
        *ngIf="state.screenName === 'Create Account'"
        [errorText]="state.error?.text || ''"
        [prefillEmail]="form.email"
        [prefillName]="form.name"
        (formUpdate)="updateForm($event)"
        (buttonClick)="continueFlow()"
      ></app-custom-welcome-screen>
    </descope>
  </div>
</div>
and created my custom screen as
Copy code
import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-custom-welcome-screen',
  templateUrl: './custom-welcome-screen.component.html',
  styleUrls: ['./custom-welcome-screen.component.scss'],
  standalone: true,
  imports: [CommonModule, FormsModule]
})
export class CustomWelcomeScreenComponent implements OnChanges {
  @Input() errorText: string = '';
  @Input() prefillEmail: string = '';
  @Input() prefillName: string = '';
  @Output() formUpdate = new EventEmitter<{ name: string; email: string }>();
  @Output() buttonClick = new EventEmitter<void>();

  form = {
    name: '',
    email: ''
  };

  ngOnChanges(changes: SimpleChanges): void {
    // Only prefill if the field is empty (don't overwrite user typing)
    if (changes['prefillEmail'] && !this.form.email) {
      this.form.email = this.prefillEmail;
    }
    if (changes['prefillName'] && !this.form.name) {
      this.form.name = this.prefillName;
    }
  }

  updateForm() {
    this.formUpdate.emit({ ...this.form });
  }

  onSubmit(event: Event) {
    event.preventDefault();
    // Emit final form state before signaling button click
    this.formUpdate.emit({ ...this.form });
    this.buttonClick.emit();
  }
}
i have also attached my flow below, how can i fix the issue
a
Hey Aksha
a
Hi
a
So you want to remove this screen and send the user directly to the create account screen right?
a
yes
but i would be using the same flow for login case or forgot password integration as well
so i might not want to change anything in the flow
i have tried to use this workaround for it to work
Copy code
import { Component, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { CustomWelcomeScreenComponent } from '../custom-welcome-screen/custom-welcome-screen.component';
import { DescopeAuthModule } from '@descope/angular-sdk';
import { firstValueFrom } from 'rxjs';

@Component({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  selector: 'app-byos-demo',
  templateUrl: './test.html',
  styleUrls: ['./test.scss'],
  standalone: true,
  imports: [CommonModule, CustomWelcomeScreenComponent, DescopeAuthModule]
})
export class ByosDemoComponent implements OnInit {
  readonly flowId: string = environment.descopeFlowId || 'passwords-with-explicit-sign-up-byos';

  state: {
    error: { text: string; type: string } | null;
    screenName?: string;
    next?: (interactionId: string, form: Record<string, unknown>) => Promise<unknown>;
  } = {
    error: null,
    screenName: undefined,
    next: undefined
  };

  form: { name: string; email: string } = { name: '', email: '' };

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private cdr: ChangeDetectorRef,
    private http: HttpClient
  ) {}

  ngOnInit(): void {
    // If Descope redirected back here with ?code=, forward to exchange-token
    const code = this.route.snapshot.queryParamMap.get('code');
    const state = this.route.snapshot.queryParamMap.get('state');
    sessionStorage.setItem('code', code ?? '');
    sessionStorage.setItem('state', state ?? '');
    // if (code) {
    //   this.router.navigate(['/exchange-token'], {
    //     queryParams: { code, state }
    //   });
    // }
  }

onScreenUpdate = (
  screenName: string,
  context: Record<string, unknown>,
  next: (interactionId: string, form: Record<string, unknown>) => Promise<unknown>
): boolean => {
  this.state = {
    ...this.state,
    ...context,
    screenName,
    next
  };
  console.log('Screen update:', screenName, this.state);
  
  if (screenName === 'Login') {
    // Automatically proceed to next step for Login screen
    next('XLmMGCy6IS', { ...this.form }).then(() => {
      console.log('Proceeded to next step');
    });
    // this.cdr.detectChanges();
    // return false; // Return false to use Descope's default screen
  }
  
  if (screenName === 'Create account') {
    this.cdr.detectChanges();
    return true; // Return true to render custom screen
  }
  
  this.cdr.detectChanges();
  return true;
};

  onSuccess(event: CustomEvent): void {
    console.log('Authentication successful:', event.detail);
  }

  onError(event: CustomEvent): void {
    console.error('Authentication error:', event);
  }

  updateForm(formData: { name: string; email: string }): void {
    this.form = { ...formData };
  }

  async continueFlow(): Promise<void> {
    try {
        console.log('Continuing flow with form data:', this.form);
      const result = await this.state.next?.('AMeyg_7npF', { ...this.form });
      console.log('Flow continued with result:', result);
      if (result) {
        // Handle successful flow continuation, e.g., navigate to dashboard
        const finalExchangeResult = await this.handleFinalExchange(sessionStorage.getItem('code') || '', sessionStorage.getItem('state'));
        if (finalExchangeResult)  {        
            console.log('Final token exchange successful:', finalExchangeResult);
            this.router.navigate(['/dashboard']);
        } else {
            console.error('Final token exchange failed');
        }
      }

    } catch (error) {
      console.error('Error continuing flow:', error);
    }
  }

    async handleFinalExchange(code: string, state: string | null): Promise<any> {
    const savedState = sessionStorage.getItem('pkce_state');
    const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
  
    if (state && savedState && state !== savedState) {
      throw new Error("Security state mismatch.");
    }
    if (!codeVerifier) {
      throw new Error("Code verifier not found. Please start the signup process again.");
    }
  
    // Let the error bubble up to onSignupFormSubmit's catch block
    return await this.exchangeCodeForToken(code, codeVerifier);
  }
  
    private async exchangeCodeForToken(code: string, codeVerifier: string): Promise<any> {
    const response = await firstValueFrom(
      <http://this.http.post|this.http.post><any>(`${environment.backendBaseUrlCPEGateway}/api/v1/auth/direct/exchange-code`, {
        code,
        codeVerifier
      })
    );
  
    if (response.data.accessToken) {
      localStorage.setItem('access_token', response.data.accessToken);
      sessionStorage.removeItem('pkce_code_verifier');
      sessionStorage.removeItem('pkce_state');
      sessionStorage.removeItem('signup_customer_id');
      sessionStorage.removeItem('signup_name');
      sessionStorage.removeItem('signup_email');
      sessionStorage.removeItem('code');
      sessionStorage.removeItem('state');
    }
  
    return response;
  }
}
can you please check if this is correct way of handling it or please suggest me better way @Atharva Joshi
a
let me check
@Akash Jivane please assist here
🙌 1
a
@Atharva Joshi Yes, Sure!
🙌 1
Can you please try updating your
onScreenUpdate
function and check if the works or not ?
Copy code
onScreenUpdate = async (screenName, context, next) => {
  this.state = {
    ...this.state,
    ...context,
    screenName,
    next,
    interactionId: context?.interactionId
  };

  // Only act on first screen
  if (screenName === 'Login') {
    const email = this.form?.email || context?.user?.loginId;


    const userExists = await this.checkIfUserExists(email);

    if (userExists) {
      const loginInteraction = context.interactions.find(i =>
        i.text.toLowerCase().includes('sign in') ||
        i.text.toLowerCase().includes('login')
      );

   
      setTimeout(() => {
        next(loginInteraction.interactionId, { loginId: email });
      }, 0);

      return false;
    }

 
    return false;
  }


  if (screenName === 'Create account') {
    return true;
  }

  return false;
};
a
okay, let me try
getting this error
Copy code
Angular is running in development mode.
descope-angular-sdk.mjs:1554 DescopeAuthModule is loaded in a child module
2index.js:2 [Descope] project-id cannot be empty, please make sure to set this attribute
jo.reduce.t.<computed> @ index.js:2Understand this error
index.js:2 [Descope] Step "Login" is waiting  
{
    "screen": {
        "id": "passwords-with-explicit-sign-up-byos/SC3A6XXWjuju9VdtBn7Wf126bSh9T",
        "state": {
            "componentsConfig": {
                "samlGroupMappings": {},
                "ssoApplications": {},
                "thirdPartyAppApproveScopes": {},
                "userRoles": {},
                "userSelectedTenant": {}
            },
            "form": {
                "spAcsUrl": "",
                "spEntityId": "",
                "spMetadataUrl": ""
            },
            "inputs": {
                "spAcsUrl": "",
                "spEntityId": "",
                "spMetadataUrl": ""
            },
            "project": {
                "name": "POC"
            }
        }
    },
    "status": "waiting",
    "stepId": "1",
    "stepName": "Login",
    "action": "",
    "error": null
}
index.js:1 ERROR TypeError: Cannot read properties of undefined (reading 'find')
    at _SignupByosComponent.<anonymous> (signup-byos.component.ts:121:51)
    at Generator.next (<anonymous>)
    at main.js:18:61
    at new ZoneAwarePromise (zone.js:2701:25)
    at __async (main.js:2:10)
    at Es.onScreenUpdate (signup-byos.component.ts:111:74)
    at Es.<anonymous> (index.js:2:94924)
    at Generator.next (<anonymous>)
    at s (index.js:1:653)
@Akash Jivane can you please check
a
It seems your SDK is not receiving your Project ID. Can you please check once?
@Aksha Malik Are you still getting the error ?
a
no it worked, thanks
a
Great!
a
Thanks
👍 1
Hey i need to perform same for forgot password in my angular component can you help me how i can setup for forgot password @Atharva Joshi @Akash Jivane
a
Hi Yes, you can implement forgot Password in your Angular component in a very similar way to what you’ve done for signup/login.
Just update your
onScreenUpdate
function -
Copy code
onScreenUpdate = (
  screenName: string,
  context: Record<string, unknown>,
  next: (interactionId: string, form: Record<string, unknown>) => Promise<unknown>
): boolean => {
  this.state = {
    ...this.state,
    ...context,
    screenName,
    next
  };

  console.log('Screen update:', screenName, this.state);

 
  if (screenName === 'Login') {
    next('XLmMGCy6IS', { ...this.form }); // existing login interaction
    return false;
  }

  
  if (screenName === 'Forgot password') {
    next('FORGOT_PASSWORD_INTERACTION_ID', {
      loginId: this.form.email
    });
    return false;
  }


  if (screenName === 'Reset password') {
    this.cdr.detectChanges();
    return true;
  }

 
  if (screenName === 'Create account') {
    this.cdr.detectChanges();
    return true;
  }

  return true;
};
a
okay, let me check
a
And call next() something like this -
Copy code
async submitNewPassword(newPassword: string): Promise<void> {
  try {
    const result = await this.state.next?.(
      'RESET_PASSWORD_INTERACTION_ID',
      { password: newPassword }
    );

    console.log('Password reset success:', result);

    if (result) {
      const exchange = await this.handleFinalExchange(
        sessionStorage.getItem('code') || '',
        sessionStorage.getItem('state')
      );

      if (exchange) {
        this.router.navigate(['/dashboard']);
      }
    }

  } catch (error) {
    console.error('Reset password failed:', error);
  }
}
Let me know if this works for you.
a
sure, let me integrate and let you know if this works
a
Sure!
🙌 1
a
@Akash Jivane ya it worked, thanks for help
a
Great!
🙌 1