Allmine API

Onboarding resume — RTK Query

~5 dkMobil / WebKararlı

Kayıt tamamlama ve completedSteps mobil akışı

Onboarding Resume - React Native (RTK Query) Dokumani

Migration rehberi

Yapılandırma sırası için Migration şablonu. Yeni düzenlemelerde Amaç → Önkoşullar → Endpoint → Request/Response → Hata kodları → Client adımları → İlgili sayfalar bölümlerini tercih edin.

Bu doküman, kullanıcının onboarding'i yarida biraktigi durumda uygulama acildiginda veya tekrar giris yaptiginda kaldigi adimdan devam etmesini saglamak için hazırlandı.

1) Hedef Davranis

Beklenen davranis:

  • Kullanıcı login olduktan sonra her zaman backend'den onboarding durumu okunur.
  • isRegistrationComplete = true ise kullanıcı direkt ana uygulama stack'ine gider.
  • isRegistrationComplete = false ise nextStep değeri hangi adimi isaret ediyorsa kullanıcı o ekrana yonlendirilir.
  • Kullanıcı uygulamayi kapatip acsa bile kaynak dogru (source of truth) backend'deki GET /onboarding/status endpointidir.

2) Backend Sozlesmesi (v1)

Onboarding endpointleri:

  • GET /api/v1/onboarding/status
  • POST /api/v1/onboarding/step-1
  • POST /api/v1/onboarding/step-2
  • POST /api/v1/onboarding/step-3
  • POST /api/v1/onboarding/step-4 (multipart/form-data, profilePhoto)
  • POST /api/v1/onboarding/step-5
  • POST /api/v1/onboarding/step-6
  • POST /api/v1/onboarding/step-7

Yardimci endpointler:

  • POST /api/v1/users/check-username (step-5 oncesi kullanıcı adi kontrolu)
  • GET /api/v1/reference-data/genders
  • GET /api/v1/reference-data/interests
  • GET /api/v1/reference-data/expertise-levels

Status response ozeti:

export type RegistrationStatus = {
  isRegistrationComplete: boolean;
  completedSteps: {
    step1: boolean;
    step2: boolean;
    step3: boolean;
    step4: boolean;
    step5: boolean;
    step6: boolean;
    step7: boolean;
  };
  nextStep: number | null;
};

Notlar:

  • nextStep = null ise tüm adimlar tamamlanmistir.
  • Tüm onboarding endpointleri auth ister (Bearer token).
  • Sunucu yanitlari BaseResponseDto<T> (yani data alani) yapisinda döner.

2.1) GET /onboarding/status response detayi

Endpoint her zaman BaseResponseDto<RegistrationStatus> formatinda döner:

export type BaseResponseDto<T> = {
  isSuccess: boolean;
  statusCode: number;
  data: T;
  errors?: string[];
  timestamp: string;
};

Ornek ham response:

{
  "isSuccess": true,
  "statusCode": 200,
  "data": {
    "isRegistrationComplete": false,
    "completedSteps": {
      "step1": true,
      "step2": true,
      "step3": false,
      "step4": false,
      "step5": false,
      "step6": false,
      "step7": false
    },
    "nextStep": 3
  },
  "timestamp": "2026-03-06T12:00:00.000Z"
}

Alan aciklamalari:

  • isRegistrationComplete: kullanıcının kaydi tamamen bitmis mi (backend'de user kaydindaki isRegistrationComplete alani).
  • completedSteps.step1: name ve surname doluysa true.
  • completedSteps.step2: gender doluysa true.
  • completedSteps.step3: birthDate doluysa true.
  • completedSteps.step4: profilePhoto varsa true.
  • completedSteps.step5: username varsa true.
  • completedSteps.step6: interests tam 5 eleman + expertise varsa true.
  • completedSteps.step7: privacyPolicyAccepted, termsOfUseAccepted, consentFormAccepted ucude true ise true.
  • nextStep: 1'den 7'ye kadar ilk eksik adim; hepsi tamam ise null.

Kritik davranis notu:

  • Backend, status hesaplanırken adimlarin tamamlanma durumunu field bazli cikarir.
  • Route kararinda pratik kural: once isRegistrationComplete, sonra nextStep kullanın.

2.2) Senaryoya gore status ornekleri

  1. Yeni kullanıcı (OTP sonrasi, onboarding'e daha baslamadi)
{
  "isRegistrationComplete": false,
  "completedSteps": {
    "step1": false,
    "step2": false,
    "step3": false,
    "step4": false,
    "step5": false,
    "step6": false,
    "step7": false
  },
  "nextStep": 1
}
  1. Yarim kalmis onboarding (ornek: step-1 ve step-2 tamam)
{
  "isRegistrationComplete": false,
  "completedSteps": {
    "step1": true,
    "step2": true,
    "step3": false,
    "step4": false,
    "step5": false,
    "step6": false,
    "step7": false
  },
  "nextStep": 3
}
  1. Onboarding tamamlandi
{
  "isRegistrationComplete": true,
  "completedSteps": {
    "step1": true,
    "step2": true,
    "step3": true,
    "step4": true,
    "step5": true,
    "step6": true,
    "step7": true
  },
  "nextStep": null
}

2.3) Hata / auth durumlari

  • 401 Unauthorized: token gecersiz/eksik.
  • Backend kullanıcıyi bulamazsa veya status hesaplama içinde hata olursa fallback olarak step-1'e yonlendirecek bir status donebilir:
{
  "isRegistrationComplete": false,
  "completedSteps": {
    "step1": false,
    "step2": false,
    "step3": false,
    "step4": false,
    "step5": false,
    "step6": false,
    "step7": false
  },
  "nextStep": 1
}

3) Step Payload Kurallari (Ozet)

  • Step-1: { name, surname } (string, min 2, max 50)
  • Step-2: { gender } (male | woman | non_binary | transgender | other | prefer_not_to_say)
  • Step-3: { birthDate } (ISO date-time, kullanıcı 18+ olmali)
  • Step-4: multipart/form-data içinde profilePhoto dosyasi (png/jpg/jpeg)
  • Step-5: { username } (3-30, regex ^[a-zA-Z0-9_]+$, benzersiz)
  • Step-6: { interests, expertise } (interests tam olarak 5 eleman olmali)
  • Step-7:
{
  privacyPolicy: { version: string; accepted: true },
  termsOfUse: { version: string; accepted: true },
  consentForm: { version: string; accepted: true }
}

4) RTK Query API Katmani Ornegi

src/store/api/onboardingApi.ts

import { baseApi } from './baseApi';

export type BaseResponseDto<T> = {
  isSuccess: boolean;
  statusCode: number;
  data: T;
  errors?: string[];
  timestamp: string;
};

export type RegistrationStatus = {
  isRegistrationComplete: boolean;
  completedSteps: {
    step1: boolean;
    step2: boolean;
    step3: boolean;
    step4: boolean;
    step5: boolean;
    step6: boolean;
    step7: boolean;
  };
  nextStep: number | null;
};

type StepOnePayload = { name: string; surname: string };
type StepTwoPayload = {
  gender: 'male' | 'woman' | 'non_binary' | 'transgender' | 'other' | 'prefer_not_to_say';
};
type StepThreePayload = { birthDate: string };
type StepFivePayload = { username: string };
type StepSixPayload = { interests: string[]; expertise: string };
type StepSevenPayload = {
  privacyPolicy: { version: string; accepted: boolean };
  termsOfUse: { version: string; accepted: boolean };
  consentForm: { version: string; accepted: boolean };
};

const unwrap = <T>(response: BaseResponseDto<T> | T): T => {
  if (response && typeof response === 'object' && 'data' in (response as any)) {
    return (response as BaseResponseDto<T>).data;
  }
  return response as T;
};

export const onboardingApi = baseApi.injectEndpoints({
  endpoints: (builder) => ({
    getOnboardingStatus: builder.query<RegistrationStatus, void>({
      query: () => ({ url: '/onboarding/status' }),
      transformResponse: (response: BaseResponseDto<RegistrationStatus> | RegistrationStatus) =>
        unwrap(response),
      providesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepOne: builder.mutation<unknown, StepOnePayload>({
      query: (body) => ({ url: '/onboarding/step-1', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepTwo: builder.mutation<unknown, StepTwoPayload>({
      query: (body) => ({ url: '/onboarding/step-2', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepThree: builder.mutation<unknown, StepThreePayload>({
      query: (body) => ({ url: '/onboarding/step-3', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepFour: builder.mutation<unknown, { file: { uri: string; type: string; name: string } }>({
      query: ({ file }) => {
        const formData = new FormData();
        formData.append('profilePhoto', file as any);

        return {
          url: '/onboarding/step-4',
          method: 'POST',
          body: formData,
        };
      },
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepFive: builder.mutation<unknown, StepFivePayload>({
      query: (body) => ({ url: '/onboarding/step-5', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepSix: builder.mutation<unknown, StepSixPayload>({
      query: (body) => ({ url: '/onboarding/step-6', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    submitStepSeven: builder.mutation<unknown, StepSevenPayload>({
      query: (body) => ({ url: '/onboarding/step-7', method: 'POST', body }),
      invalidatesTags: [{ type: 'OnboardingStatus', id: 'ME' }],
    }),

    checkUsername: builder.mutation<{ available: boolean; reason?: string }, { username: string }>({
      query: (body) => ({
        url: '/users/check-username',
        method: 'POST',
        body,
      }),
      transformResponse: (
        response: BaseResponseDto<{ available: boolean; reason?: string }> | { available: boolean; reason?: string },
      ) => unwrap(response),
    }),
  }),
});

export const {
  useGetOnboardingStatusQuery,
  useLazyGetOnboardingStatusQuery,
  useSubmitStepOneMutation,
  useSubmitStepTwoMutation,
  useSubmitStepThreeMutation,
  useSubmitStepFourMutation,
  useSubmitStepFiveMutation,
  useSubmitStepSixMutation,
  useSubmitStepSevenMutation,
  useCheckUsernameMutation,
} = onboardingApi;

baseApi için not:

  • baseUrl değeriniz /api/v1 ile bitiyorsa yukaridaki url alanlari dogrudur.
  • baseUrl değeriniz /api ile bitiyorsa endpointleri /v1/onboarding/... seklinde verin.
  • tagTypes içinde OnboardingStatus tanimli olmali.

5) Navigation Gate (Kaldigi Yerden Devam)

Onboarding resume için en kritik nokta: route kararini tek yerden vermek.

src/navigation/useOnboardingGate.ts

import { useMemo } from 'react';
import { useGetOnboardingStatusQuery } from '@/store/api/onboardingApi';
import { useAppSelector } from '@/store/hooks';

export const useOnboardingGate = () => {
  const isLoggedIn = useAppSelector((s) => !!s.auth.accessToken);

  const { data, isLoading, isFetching, refetch } = useGetOnboardingStatusQuery(undefined, {
    skip: !isLoggedIn,
    refetchOnFocus: true,
    refetchOnReconnect: true,
  });

  const decision = useMemo(() => {
    if (!isLoggedIn) return { stack: 'Auth' as const };
    if (isLoading || isFetching) return { stack: 'Splash' as const };

    if (data?.isRegistrationComplete) {
      return { stack: 'MainApp' as const };
    }

    const step = data?.nextStep ?? 1;
    return { stack: 'Onboarding' as const, step };
  }, [isLoggedIn, isLoading, isFetching, data]);

  return { decision, refetchStatus: refetch, status: data };
};

Root navigator kullanimi:

const { decision } = useOnboardingGate();

if (decision.stack === 'Splash') return <SplashScreen />;
if (decision.stack === 'Auth') return <AuthStack />;
if (decision.stack === 'Onboarding') {
  return <OnboardingStack initialStep={decision.step} />;
}
return <MainAppStack />;

6) Step Submit Sonrasi Akis

Her step kaydindan sonra dogru sonraki ekrana gecmek için 2 guvenli yol:

  1. submitStepX success oldugunda getOnboardingStatus query'sini refetch et.
  2. Donen nextStep değerine gore navigate et.

Ornek:

const [submitStepOne, { isLoading }] = useSubmitStepOneMutation();
const [getStatus] = useLazyGetOnboardingStatusQuery();

const onContinue = async (payload: { name: string; surname: string }) => {
  await submitStepOne(payload).unwrap();
  const status = await getStatus().unwrap();

  if (status.isRegistrationComplete) {
    navigation.reset({ index: 0, routes: [{ name: 'MainApp' as never }] });
    return;
  }

  navigation.navigate(`OnboardingStep${status.nextStep ?? 1}` as never);
};

7) Dayaniklilik ve Edge Case Kurallari

  • Uygulama foreground oldugunda status'u tekrar çekin.
  • Herhangi bir API çağrısında 403 registration-incomplete hatasi alınirsa onboarding gate'e geri donun.
  • Step-6 için interests.length kesinlikle 5 olmali, aksi halde 400 döner.
  • Step-7'de uc consent de accepted = true olmali, aksi halde 400 döner.
  • Step-4'te form field adi tam olarak profilePhoto olmali.

8) Onerilen UX Akisi

  • Login success -> GET /onboarding/status
  • Incomplete ise -> nextStep ekranina yonlendir
  • Her step success -> status refetch -> sonraki step
  • Step-7 success ve complete -> MainApp stack'e reset
  • Kullanıcı tekrar login/app relaunch -> yine status'a gore route ver

Bu yapi ile onboarding resume davranisi deterministic olur ve local state kaymasi yasamazsiniz.

On this page