Premium content — RTK Query
Premium içerik mobil API entegrasyonu
Premium Content - RTK Query ve React Native Implementasyon Rehberi
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, mobil tarafta premium content ozelligini sadece kullanıcıya ozel listeleme ile nasil entegre edeceginizi anlatır.
Bu modelde kullanıcılar tüm premium content listesini çekmez:
- Baska bir kullanıcının profilinde:
GET /premium-content/user/:ownerId - Kendi icerik yonetim ekraninda:
GET /premium-content/me
Liste endpointlerinde varsayilan siralama createdAt desc (en yeni -> en eski) olarak calisir.
GET /premium-content/user/:ownerId endpointinde ek olarak, istek atan kullanıcının
unlock ettigi ucretli icerikler sayfa içinde listenin en sonuna tasinir.
Icindekiler
- Genel Akis
- Backend Endpoint Sozlesmeleri
- RTK Query API Tanimi
- React Native Ekran Akisi
- Upload (multipartform-data) Detayi
- Hata Yonetimi
- Onerilen Test Senaryolari
Genel Akis
Profil bazli goruntuleme akisi:
- Kullanıcı bir profile girer.
- Profil premium icerikleri
GET /premium-content/user/:ownerIdile çekilir. - Ucretli ve acilmamis icerikte
originalUrlgelmez,previewUrlkullanılır. - Icerik detay/erisim için
GET /premium-content/:id/accesscagrilir. - Kilit acma için
POST /premium-content/:id/unlockcagrilir. - Basarili unlock sonrasi ayni profil listesinde
originalUrlgorunur.
Kendi iceriklerini yonetme akisi:
GET /premium-content/meile kendi iceriklerini çek.POST /premium-contentile icerik olustur.PATCH /premium-content/:idile icerik guncelle.DELETE /premium-content/:idile soft delete yap.
Backend Endpoint Sozlesmeleri
Tüm yanitlar
BaseResponseDtozarfi ile gelir:
{
"isSuccess": true,
"statusCode": 200,
"data": {},
"errors": [],
"timestamp": "2026-03-06T10:00:00.000Z"
}PremiumContentResponseDto:
{
_id: string;
owner: string;
previewUrl: string;
originalUrl?: string; // kilitliyse gelmeyebilir
price: number;
order: number;
createdAt: string;
updatedAt: string;
}PaginationDto:
{
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
hasNextPage?: boolean;
hasPrevPage?: boolean;
}PaginatedPremiumContentResponse:
{
list: PremiumContentResponseDto[];
pagination: PaginationDto;
}1) Belirli kullanıcının premium icerikleri (USER)
GET /api/v1/premium-content/user/:ownerId
Auth: Bearer token (Role.USER)
Query:
page?: number(default:1, min:1)limit?: number(default:20, min:1, max:100)
data tipi: PaginatedPremiumContentResponse
Profil ekranlarinda ana liste endpoint budur. Ek siralama kurali: Kullanıcı tarafından daha once unlock edilmis ucretli icerikler en sonda gelir.
2) Kendi premium iceriklerim (USER)
GET /api/v1/premium-content/me
Auth: Bearer token (Role.USER)
Query:
page?: number(default:1, min:1)limit?: number(default:20, min:1, max:100)
data tipi: PaginatedPremiumContentResponse
3) Icerik erisim durumu (USER)
GET /api/v1/premium-content/:id/access
Auth: Bearer token (Role.USER)
data tipi:
{
contentId: string;
url: string; // kilitliyse previewUrl, aciksa originalUrl
previewUrl: string;
requiresPurchase: boolean;
price: number;
isUnlocked: boolean;
}Not: Public detay endpointi
GET /api/v1/premium-content/:id endpointi public calisir ve ucretli iceriklerde
originalUrl alanini dönmez. Ucretli kayitlarda sadece previewUrl kullanılmalıdır.
4) Icerik unlock (USER)
POST /api/v1/premium-content/:id/unlock
Auth: Bearer token (Role.USER)
data tipi:
{
contentId: string;
url: string; // artık originalUrl
previewUrl: string;
remainingCredits?: number;
}5) Icerik oluşturma (USER)
POST /api/v1/premium-content
Auth: Bearer token (Role.USER)
Content-Type: multipart/form-data
Alanlar:
price(zorunlu, number >= 0)order(opsiyonel, int >= 0)originalUrl(opsiyonel)previewUrl(opsiyonel)file(opsiyonel, binary)
6) Icerik guncelleme (USER)
PATCH /api/v1/premium-content/:id
Auth: Bearer token (Role.USER)
Content-Type: multipart/form-data
7) Icerik soft delete (USER)
DELETE /api/v1/premium-content/:id
Auth: Bearer token (Role.USER)
8) Admin liste endpointi (opsiyonel)
GET /api/v1/premium-content/admin
Auth: Bearer token (Role.ADMIN)
Query:
page?: number(default:1, min:1)limit?: number(default:20, min:1, max:100)
data tipi: PaginatedPremiumContentResponse
RTK Query API Tanimi
import { baseApi } from "../baseApi";
export type PremiumContentItem = {
_id: string;
owner: string;
previewUrl: string;
originalUrl?: string;
price: number;
order: number;
createdAt: string;
updatedAt: string;
};
export type Pagination = {
currentPage: number;
totalPages: number;
totalItems: number;
itemsPerPage: number;
hasNextPage?: boolean;
hasPrevPage?: boolean;
};
export type PaginatedPremiumContents = {
list: PremiumContentItem[];
pagination: Pagination;
};
export type PremiumContentListQuery = {
page?: number;
limit?: number;
};
export type PremiumAccessResponse = {
contentId: string;
url: string;
previewUrl: string;
requiresPurchase: boolean;
price: number;
isUnlocked: boolean;
};
export type UnlockPremiumContentResponse = {
contentId: string;
url: string;
previewUrl: string;
remainingCredits?: number;
};
export type BaseResponse<T> = {
isSuccess: boolean;
statusCode: number;
data?: T;
errors?: string[];
timestamp: string;
};
export const premiumContentApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getPremiumContentsByOwner: builder.query<
PaginatedPremiumContents,
{ ownerId: string } & PremiumContentListQuery
>({
query: ({ ownerId, page = 1, limit = 20 }) => ({
url: `/premium-content/user/${ownerId}`,
method: "GET",
params: { page, limit },
}),
transformResponse: (res: BaseResponse<PaginatedPremiumContents>) =>
res.data ?? {
list: [],
pagination: {
currentPage: 1,
totalPages: 0,
totalItems: 0,
itemsPerPage: 20,
hasNextPage: false,
hasPrevPage: false,
},
},
providesTags: (_result, _error, { ownerId }) => [
{ type: "PremiumContentByOwner", id: ownerId },
],
}),
getMyPremiumContents: builder.query<PaginatedPremiumContents, PremiumContentListQuery | void>({
query: (args) => ({
url: "/premium-content/me",
method: "GET",
params: {
page: args?.page ?? 1,
limit: args?.limit ?? 20,
},
}),
transformResponse: (res: BaseResponse<PaginatedPremiumContents>) =>
res.data ?? {
list: [],
pagination: {
currentPage: 1,
totalPages: 0,
totalItems: 0,
itemsPerPage: 20,
hasNextPage: false,
hasPrevPage: false,
},
},
providesTags: ["PremiumContentMine"],
}),
getPremiumContentAccess: builder.query<PremiumAccessResponse, string>({
query: (id) => ({ url: `/premium-content/${id}/access`, method: "GET" }),
transformResponse: (res: BaseResponse<PremiumAccessResponse>) => {
if (!res.data) throw new Error("Premium access data missing");
return res.data;
},
providesTags: (_result, _error, id) => [{ type: "PremiumContentAccess", id }],
}),
unlockPremiumContent: builder.mutation<
UnlockPremiumContentResponse,
{ id: string; ownerId?: string }
>({
query: ({ id }) => ({
url: `/premium-content/${id}/unlock`,
method: "POST",
body: {},
}),
transformResponse: (res: BaseResponse<UnlockPremiumContentResponse>) => {
if (!res.data) throw new Error("Unlock response data missing");
return res.data;
},
invalidatesTags: (_result, _error, { id, ownerId }) => [
{ type: "PremiumContentAccess", id },
"PremiumContentMine",
"ProfileBalance",
...(ownerId ? [{ type: "PremiumContentByOwner" as const, id: ownerId }] : []),
],
}),
createPremiumContent: builder.mutation<PremiumContentItem, FormData>({
query: (formData) => ({
url: "/premium-content",
method: "POST",
body: formData,
}),
transformResponse: (res: BaseResponse<PremiumContentItem>) => {
if (!res.data) throw new Error("Create response data missing");
return res.data;
},
invalidatesTags: ["PremiumContentMine"],
}),
updatePremiumContent: builder.mutation<
PremiumContentItem,
{ id: string; formData: FormData }
>({
query: ({ id, formData }) => ({
url: `/premium-content/${id}`,
method: "PATCH",
body: formData,
}),
transformResponse: (res: BaseResponse<PremiumContentItem>) => {
if (!res.data) throw new Error("Update response data missing");
return res.data;
},
invalidatesTags: (_result, _error, { id }) => [
"PremiumContentMine",
{ type: "PremiumContentAccess", id },
],
}),
deletePremiumContent: builder.mutation<{ isDeleted: boolean; message: string }, string>({
query: (id) => ({
url: `/premium-content/${id}`,
method: "DELETE",
}),
transformResponse: (res: BaseResponse<{ isDeleted: boolean; message: string }>) => {
if (!res.data) throw new Error("Delete response data missing");
return res.data;
},
invalidatesTags: ["PremiumContentMine"],
}),
}),
overrideExisting: false,
});
export const {
useGetPremiumContentsByOwnerQuery,
useGetMyPremiumContentsQuery,
useGetPremiumContentAccessQuery,
useUnlockPremiumContentMutation,
useCreatePremiumContentMutation,
useUpdatePremiumContentMutation,
useDeletePremiumContentMutation,
} = premiumContentApi;baseApi tagTypes:
tagTypes: ["PremiumContentByOwner", "PremiumContentMine", "PremiumContentAccess", "ProfileBalance"]React Native Ekran Akisi
1) Profil premium icerik listesi
- Kullanıcı profil sayfasinda
useGetPremiumContentsByOwnerQuery({ ownerId, page, limit })cagrilir. - Item render:
originalUrlvarsa göster.- yoksa
previewUrl+ kilit badge göster.
const { data, isLoading, refetch } = useGetPremiumContentsByOwnerQuery({
ownerId: profileUserId,
page: 1,
limit: 20,
});
const contents = data?.list ?? [];
const pagination = data?.pagination;2) Kendi iceriklerim ekrani
- Kendi profil/yonetim ekraninda
useGetMyPremiumContentsQuery({ page, limit })cagrilir.
const { data } = useGetMyPremiumContentsQuery({ page: 1, limit: 20 });
const myContents = data?.list ?? [];3) Detay/Unlock ekrani
useGetPremiumContentAccessQuery(contentId)ile kilit durumu okunur.- Kilitliyse
useUnlockPremiumContentMutation()tetiklenir.
const [unlockContent] = useUnlockPremiumContentMutation();
const handleUnlock = async () => {
await unlockContent({ id: contentId, ownerId: profileUserId }).unwrap();
};4) Icerik oluşturma / guncelleme / silme
- Olusturma:
useCreatePremiumContentMutation() - Güncelleme:
useUpdatePremiumContentMutation() - Silme:
useDeletePremiumContentMutation()
Bu mutasyonlar PremiumContentMine tag'ini invalidate ederek kendi icerik ekranini gunceller.
Upload (multipartform-data) Detayi
React Native tarafında:
- Numerik alanlari string gönderin (
price,order). fileiçin{ uri, type, name }objesi gönderin.Content-Typeheader'ini elle set etmeyin, boundary otomatik olussun.- iOS/Android URI formatlarini normalize edin.
Hata Yonetimi
Onerilen mapping:
400: validation / yetersiz bakiye / media hatasi401: token gecersiz / login yok403: sahiplik/yetki hatasi404: icerik bulunamadi
try {
await unlockContent({ id, ownerId }).unwrap();
} catch (err: any) {
const message = err?.data?.errors?.[0] ?? "Beklenmeyen hata";
showToast(message);
}Onerilen Test Senaryolari
- Profilde
ownerIdbazli listepage/limitile dogru kayitlari donduruyor mu? - Kendi iceriklerim ekrani
GET /premium-content/me?page=1&limit=20ile calisiyor mu? - Liste response'unda
data.listvedata.paginationdogru geliyor mu? - Varsayilan siralama en yeni -> en eski (
createdAt desc) mi? - Ucretli ve unlock edilmemis icerikte
originalUrlgizleniyor mu? - Unlock sonrasi ayni profil listesinde
originalUrlgörünüyor mu? - Yetersiz bakiyede dogru hata mesaji geliyor mu?
- Create/update multipart upload iOS ve Android'de calisiyor mu?
- Soft delete sonrasi kendi iceriklerim listesinden icerik dusuyor mu?
Not
Mobil tarafta tüm premium icerikleri listeleme (GET /premium-content) kullanilmamali.
Esas kullanim:
- Profil odakli goruntuleme:
GET /premium-content/user/:ownerId?page=1&limit=20 - Kendi iceriklerim:
GET /premium-content/me?page=1&limit=20 - Icerik erisim/unlock:
GET /premium-content/:id/accessvePOST /premium-content/:id/unlock
Liste endpointlerinde data alani dogrudan dizi değil, list + pagination nesnesidir.