Allmine API

Premium content — RTK Query

Sıra 4~5 dkMobil / WebKararlı

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

  1. Genel Akis
  2. Backend Endpoint Sozlesmeleri
  3. RTK Query API Tanimi
  4. React Native Ekran Akisi
  5. Upload (multipartform-data) Detayi
  6. Hata Yonetimi
  7. Onerilen Test Senaryolari

Genel Akis

Profil bazli goruntuleme akisi:

  1. Kullanıcı bir profile girer.
  2. Profil premium icerikleri GET /premium-content/user/:ownerId ile çekilir.
  3. Ucretli ve acilmamis icerikte originalUrl gelmez, previewUrl kullanılır.
  4. Icerik detay/erisim için GET /premium-content/:id/access cagrilir.
  5. Kilit acma için POST /premium-content/:id/unlock cagrilir.
  6. Basarili unlock sonrasi ayni profil listesinde originalUrl gorunur.

Kendi iceriklerini yonetme akisi:

  1. GET /premium-content/me ile kendi iceriklerini çek.
  2. POST /premium-content ile icerik olustur.
  3. PATCH /premium-content/:id ile icerik guncelle.
  4. DELETE /premium-content/:id ile soft delete yap.

Backend Endpoint Sozlesmeleri

Tüm yanitlar BaseResponseDto zarfi 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:
    • originalUrl varsa 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:

  1. Numerik alanlari string gönderin (price, order).
  2. file için { uri, type, name } objesi gönderin.
  3. Content-Type header'ini elle set etmeyin, boundary otomatik olussun.
  4. iOS/Android URI formatlarini normalize edin.

Hata Yonetimi

Onerilen mapping:

  • 400: validation / yetersiz bakiye / media hatasi
  • 401: token gecersiz / login yok
  • 403: sahiplik/yetki hatasi
  • 404: icerik bulunamadi
try {
  await unlockContent({ id, ownerId }).unwrap();
} catch (err: any) {
  const message = err?.data?.errors?.[0] ?? "Beklenmeyen hata";
  showToast(message);
}

Onerilen Test Senaryolari

  1. Profilde ownerId bazli liste page/limit ile dogru kayitlari donduruyor mu?
  2. Kendi iceriklerim ekrani GET /premium-content/me?page=1&limit=20 ile calisiyor mu?
  3. Liste response'unda data.list ve data.pagination dogru geliyor mu?
  4. Varsayilan siralama en yeni -> en eski (createdAt desc) mi?
  5. Ucretli ve unlock edilmemis icerikte originalUrl gizleniyor mu?
  6. Unlock sonrasi ayni profil listesinde originalUrl görünüyor mu?
  7. Yetersiz bakiyede dogru hata mesaji geliyor mu?
  8. Create/update multipart upload iOS ve Android'de calisiyor mu?
  9. 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/access ve POST /premium-content/:id/unlock

Liste endpointlerinde data alani dogrudan dizi değil, list + pagination nesnesidir.

On this page