Seller Registration and Verification Flow

Seller registration and verification is the onboarding funnel that converts buyers into active sellers. A frictionless registration (just a few extra fields) maximises conversion; the verification step (admin reviews and approves) ensures trust and safety. The complete flow โ€” registration โ†’ dashboard โ†’ first listing โ†’ approval โ†’ live โ€” demonstrates the full classified website working end-to-end with authentication, role assignment, and the listing lifecycle.

Seller Registration Flow

// โ”€โ”€ seller-registration.component.ts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@Component({
  selector:   'app-seller-registration',
  standalone:  true,
  imports:    [ReactiveFormsModule, MatFormFieldModule, MatInputModule,
               MatButtonModule, MatCheckboxModule],
  template: `
    <div class="registration-page">
      <h1>Become a Seller</h1>
      <p>Start listing your items and reaching thousands of buyers.</p>

      @if (registered()) {
        <!-- Success state โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
        <div class="success-card" data-cy="registration-success">
          <mat-icon color="primary">check_circle</mat-icon>
          <h2>You're now a seller!</h2>
          <p>You can start creating listings straight away.</p>
          <p class="verification-note">
            For additional trust badges and unlimited listings,
            <strong>apply for seller verification</strong> from your dashboard.
          </p>
          <button mat-raised-button color="primary"
                  routerLink="/my-listings/new">
            Create Your First Listing
          </button>
        </div>
      } @else {
        <!-- Registration form โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -->
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
          <mat-form-field>
            <mat-label>Business / Display Name</mat-label>
            <input matInput formControlName="sellerName" data-cy="seller-name">
            <mat-error *ngIf="form.get('sellerName')?.errors?.['required']">
              Display name is required
            </mat-error>
          </mat-form-field>

          <mat-form-field>
            <mat-label>Contact Phone</mat-label>
            <input matInput formControlName="phone" type="tel">
          </mat-form-field>

          <mat-checkbox formControlName="agreedToTerms" data-cy="agree-terms">
            I agree to the <a routerLink="/terms">seller terms</a>
          </mat-checkbox>

          @if (errorMessage()) {
            <mat-error>{{ errorMessage() }}</mat-error>
          }

          <button mat-raised-button color="primary" type="submit"
                  [disabled]="form.invalid || submitting()"
                  data-cy="register-seller-btn">
            @if (submitting()) { Registering... } @else { Become a Seller }
          </button>
        </form>
      }
    </div>
  `,
})
export class SellerRegistrationComponent {
  private api    = inject(AuthApiService);
  private auth   = inject(AuthService);
  private notify = inject(ToastService);

  registered   = signal(false);
  submitting   = signal(false);
  errorMessage = signal<string | null>(null);

  form = inject(FormBuilder).nonNullable.group({
    sellerName:    ['', [Validators.required, Validators.minLength(2)]],
    phone:         [''],
    agreedToTerms: [false, Validators.requiredTrue],
  });

  onSubmit(): void {
    if (this.form.invalid) return;
    this.submitting.set(true);
    this.errorMessage.set(null);

    this.api.registerAsSeller(this.form.getRawValue()).subscribe({
      next: () => {
        // Refresh auth session to pick up the new Seller role in the JWT
        this.auth.refreshSession().subscribe(() => {
          this.registered.set(true);
          this.submitting.set(false);
        });
      },
      error: (err: ApiError) => {
        this.submitting.set(false);
        this.errorMessage.set(
          err.status === 409
            ? 'You are already registered as a seller.'
            : 'Registration failed. Please try again.'
        );
      },
    });
  }
}

// โ”€โ”€ Seller verification status banner โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// seller-dashboard.component.html:
// @if (!auth.isVerifiedSeller()) {
//   <div class="verification-banner" data-cy="verification-banner">
//     <mat-icon>verified</mat-icon>
//     <div>
//       <strong>Get Verified</strong>
//       <p>Verified sellers get a trust badge, unlimited listings, and higher placement.</p>
//     </div>
//     <button mat-stroked-button (click)="applyForVerification()">Apply Now</button>
//   </div>
// } @else {
//   <div class="verified-badge">
//     <mat-icon color="primary">verified</mat-icon>
//     Verified Seller
//   </div>
// }
Note: After successful seller registration, the Angular app calls auth.refreshSession() to get a new JWT that includes the Seller role. Without this refresh, the user’s current JWT still has only the Buyer role and they cannot access seller features until their token expires (up to 15 minutes). The refresh session call exchanges the refresh token cookie for a new access token โ€” seamless to the user, immediately effective. Always refresh the session after role assignments that the user should feel immediately.
Tip: The agreedToTerms: [false, Validators.requiredTrue] pattern is the clean way to require a checkbox to be checked โ€” Validators.requiredTrue validates that the boolean value is exactly true (not just truthy). The submit button is disabled while form.invalid โ€” including while the checkbox is unchecked. This ensures sellers cannot accidentally register without agreeing to terms, and the validation state prevents the form from being submitted programmatically without the agreement.
Warning: The seller registration endpoint assigns the Seller role in the database. This is a privilege escalation โ€” any authenticated user can become a seller. Ensure the endpoint has robust abuse prevention: rate limiting (can’t spam register/unregister), audit logging (who registered as a seller and when), and terms agreement recording (store the fact that the user agreed to terms, with the terms version and timestamp, for legal compliance). Never trust the client’s claim about agreeing to terms โ€” record it server-side.

Full Registration-to-First-Listing Flow

Step User Action System Response
1 Creates account (buyer) JWT with Buyer role issued
2 Clicks “Become a Seller” POST /api/auth/register-seller โ†’ Seller role added
3 Session refreshed New JWT with Seller role
4 Creates listing (draft) POST /api/listings โ†’ Status: PendingReview
5 Moderator approves PATCH /api/admin/listings/{id}/moderate โ†’ Status: Active
6 Email: “Your listing is live” ListingPublishedEvent โ†’ SendPublishedEmailHandler
7 Buyer contacts seller POST /api/listings/{id}/contact โ†’ 202 Accepted
8 Seller replies PUT /api/contact-requests/{id}/reply โ†’ 204

Common Mistakes

Mistake 1 โ€” Not refreshing the session after seller role assignment (new role not in JWT)

โŒ Wrong โ€” no refresh call; user stays on the success screen but cannot create listings; old JWT has Buyer role only.

โœ… Correct โ€” auth.refreshSession() immediately after successful registration; new JWT has Seller role; features unlocked.

โŒ Wrong โ€” checkbox checked on the client only; no server record of when and which version of terms was agreed to.

โœ… Correct โ€” server records agreement: userId, termsVersion, agreedAt timestamp; legally defensible record.

🧠 Test Yourself

A new seller submits their first listing. The backend sets status to PendingReview. The seller dashboard shows “PendingReview” with an amber badge. The seller then clicks “Publish” (which appears for Draft listings only). Should the Publish button appear for PendingReview listings?