// routes/onboarding/index.tsx import { define } from "../../utils.ts"; import { page } from "fresh"; import type { SessionCookie } from "../auth/callback.ts"; import type { Property } from "../../db/interfaces.ts"; import OnboardingHashGate from "../../islands/OnboardingHashGate.tsx"; import LoginPurposeButton from "../../islands/LoginPurposeButton.tsx"; import { getOnboardingStatus, markRoleOnboarded, startGuestOnboarding, } from "../../services/onboardingService.ts"; import { getAllProperties, getPropertyById, } from "../../services/propertyService.ts"; import { getTenantInviteById } from "../../services/tenantInviteService.ts"; import { createTenantApplication, hasUserAppliedForProperty, } from "../../services/tenantApplicationService.ts"; interface OnboardingPageData { stateData: SessionCookie; properties: Property[]; onboardingStatus: Awaited>; error?: string; selectedPropertyId?: string; inviteId?: string; } async function buildOnboardingPage( ctx: { state: { data: SessionCookie } }, options: { error?: string; selectedPropertyId?: string; inviteId?: string } = {}, ) { const stateData = ctx.state.data as SessionCookie; const userId = stateData?.user_id; let properties: Property[] = []; if (userId) { properties = await getAllProperties(); } else if (options.selectedPropertyId) { const selectedProperty = await getPropertyById( options.selectedPropertyId, ); if (selectedProperty) { properties = [selectedProperty]; } } const onboardingStatus = userId ? await getOnboardingStatus(userId) : null; return page({ stateData, properties, onboardingStatus, error: options.error, selectedPropertyId: options.selectedPropertyId, inviteId: options.inviteId, }); } export const handler = define.handlers({ async GET(ctx) { const url = new URL(ctx.req.url); const inviteId = url.searchParams.get("invite_id") || undefined; let selectedPropertyId = url.searchParams.get("property_id") || undefined; let error: string | undefined; if (inviteId) { const invite = await getTenantInviteById(inviteId); if (!invite) { error = "We could not find that invite. Please ask your landlord to resend it."; } else if (invite.property_id) { selectedPropertyId = invite.property_id; } } return await buildOnboardingPage(ctx, { selectedPropertyId, error, inviteId, }); }, async POST(ctx) { const stateData = ctx.state.data as SessionCookie; const { user_id } = stateData; if (!user_id) { return new Response("", { status: 302, headers: { Location: "/auth/login" }, }); } const formData = await ctx.req.formData(); const sender = (formData.get("sender") || "").toString(); if (sender === "landlord_start") { const status = await getOnboardingStatus(user_id); if (status?.onboarding_step && status.onboarding_step >= 2) { return new Response("", { status: 302, headers: { Location: "/landlord/onboarding/property-setup" }, }); } if (status?.onboarding_step && status.onboarding_step > 0) { return new Response("", { status: 302, headers: { Location: "/landlord/onboarding/profile" }, }); } if (status?.onboarded_landlord) { return new Response("", { status: 302, headers: { Location: "/landlord/properties/add" }, }); } const result = await startGuestOnboarding(user_id, "landlord"); return new Response("", { status: 302, headers: { Location: result.nextUrl }, }); } if (sender === "tenant_start") { const propertyId = (formData.get("property_id") || "").toString(); if (!propertyId) { return await buildOnboardingPage(ctx, { error: "Select a property to continue your tenant onboarding.", }); } await startGuestOnboarding(user_id, "tenant"); const alreadyApplied = await hasUserAppliedForProperty( propertyId, user_id, ); if (!alreadyApplied) { const payload = JSON.stringify({ source: "onboarding", requested_at: Date.now(), }); await createTenantApplication(propertyId, user_id, payload); } await markRoleOnboarded(user_id, "tenant"); return new Response("", { status: 302, headers: { Location: "/tenant/dashboard" }, }); } return new Response("", { status: 302, headers: { Location: "/" } }); }, }); export default define.page(function OnboardingPage({ data }) { const { stateData, properties, onboardingStatus, error, selectedPropertyId, inviteId, } = data as OnboardingPageData; const isLoggedIn = Boolean(stateData?.user_id); const isInviteLink = Boolean(inviteId); const tenantOnboarded = Boolean(onboardingStatus?.onboarded_tenant); const landlordStep = onboardingStatus?.onboarding_step ?? 0; const landlordReady = Boolean(onboardingStatus?.onboarded_landlord) && landlordStep === 0; const landlordInProgress = landlordStep > 0; const selectedProperty = selectedPropertyId ? properties.find((property) => property.id === selectedPropertyId) : undefined; const showPropertySelect = isLoggedIn && !isInviteLink; const availableProperties = properties.filter((property) => property.rental_status === "available" ); const inviteRoute = inviteId ? `/onboarding?invite_id=${inviteId}#tenant` : null; let tenantOptions = showPropertySelect ? (availableProperties.length > 0 ? availableProperties : properties) : []; if ( showPropertySelect && selectedProperty && !tenantOptions.some((property) => property.id === selectedProperty.id) ) { tenantOptions = [selectedProperty, ...tenantOptions]; } const selectedPropertyLabel = selectedProperty ? (selectedProperty.title || selectedProperty.address_1 || "Property") : ""; const selectedPropertyAddress = selectedProperty ? [ selectedProperty.address_1, selectedProperty.address_2, selectedProperty.city, selectedProperty.state, selectedProperty.zip, ].filter(Boolean).join(", ") : ""; return (

Real Estate Onboarding

Choose your property path.

Start a new listing as a landlord, or join a property as a tenant. You can switch between roles anytime and manage multiple properties from the same account.

{!isLoggedIn && (

Sign in to begin onboarding. Use the home page to log in and return here with your role pre-selected.

)}
{error && (
{error}
)}

Landlord Setup

Launch a new property, define rent schedules, and invite tenants.

{landlordReady ? "Ready" : landlordInProgress ? "In Progress" : "New"}
  • - Capture property details and lease terms.
  • - Track applicants, maintenance, and payments.
  • - Add as many properties as you manage.
{isLoggedIn ? (
) : ( Sign In to Begin )} {landlordReady && ( Open Landlord Dashboard )}

Tenant Setup

Apply to a property and unlock rent payments, messaging, and service requests.

{tenantOnboarded ? "Active" : "New"}
  • - Choose a property to apply in seconds.
  • - Keep rent, docs, and maintenance in one place.
  • - Track your application status in the tenant portal.
{selectedProperty && (

Invite selected rental

{selectedPropertyLabel}

{selectedPropertyAddress && (

{selectedPropertyAddress}

)}
)} {selectedPropertyId && !selectedProperty && (
We could not find that property. Ask your landlord to resend the invite link.
)} {isLoggedIn ? (
{isInviteLink ? (

Invited property

{selectedProperty ? ( <>

{selectedPropertyLabel}

{selectedPropertyAddress && (

{selectedPropertyAddress}

)} ) : (

We could not find the property on this invite.

)}
) : (
{tenantOptions.length === 0 && (

No properties are listed yet. Check back soon or ask your landlord to add one.

)}
)} {tenantOnboarded && ( Open Tenant Dashboard )}
) : (
Sign In to Apply
)}
); });