Onboarding resume — RTK Query
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 = trueise kullanıcı direkt ana uygulama stack'ine gider.isRegistrationComplete = falseisenextStepdeğ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/statusendpointidir.
2) Backend Sozlesmesi (v1)
Onboarding endpointleri:
GET /api/v1/onboarding/statusPOST /api/v1/onboarding/step-1POST /api/v1/onboarding/step-2POST /api/v1/onboarding/step-3POST /api/v1/onboarding/step-4(multipart/form-data,profilePhoto)POST /api/v1/onboarding/step-5POST /api/v1/onboarding/step-6POST /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/gendersGET /api/v1/reference-data/interestsGET /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 = nullise tüm adimlar tamamlanmistir.- Tüm onboarding endpointleri auth ister (
Bearer token). - Sunucu yanitlari
BaseResponseDto<T>(yanidataalani) 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 kaydindakiisRegistrationCompletealani).completedSteps.step1:namevesurnamedoluysatrue.completedSteps.step2:genderdoluysatrue.completedSteps.step3:birthDatedoluysatrue.completedSteps.step4:profilePhotovarsatrue.completedSteps.step5:usernamevarsatrue.completedSteps.step6:intereststam 5 eleman +expertisevarsatrue.completedSteps.step7:privacyPolicyAccepted,termsOfUseAccepted,consentFormAccepteducudetrueisetrue.nextStep: 1'den 7'ye kadar ilk eksik adim; hepsi tamam isenull.
Kritik davranis notu:
- Backend, status hesaplanırken adimlarin tamamlanma durumunu field bazli cikarir.
- Route kararinda pratik kural: once
isRegistrationComplete, sonranextStepkullanın.
2.2) Senaryoya gore status ornekleri
- 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
}- 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
}- 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-dataiçindeprofilePhotodosyasi (png/jpg/jpeg) - Step-5:
{ username }(3-30, regex^[a-zA-Z0-9_]+$, benzersiz) - Step-6:
{ interests, expertise }(intereststam 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:
baseUrldeğeriniz/api/v1ile bitiyorsa yukaridakiurlalanlari dogrudur.baseUrldeğeriniz/apiile bitiyorsa endpointleri/v1/onboarding/...seklinde verin.tagTypesiçindeOnboardingStatustanimli 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:
submitStepXsuccess oldugundagetOnboardingStatusquery'sinirefetchet.- Donen
nextStepdeğ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
foregroundoldugunda status'u tekrar çekin. - Herhangi bir API çağrısında
403 registration-incompletehatasi alınirsa onboarding gate'e geri donun. - Step-6 için
interests.lengthkesinlikle5olmali, aksi halde 400 döner. - Step-7'de uc consent de
accepted = trueolmali, aksi halde 400 döner. - Step-4'te form field adi tam olarak
profilePhotoolmali.
8) Onerilen UX Akisi
- Login success ->
GET /onboarding/status - Incomplete ise ->
nextStepekranina yonlendir - Her step success -> status refetch -> sonraki step
- Step-7 success ve complete ->
MainAppstack'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.