Token Sistemi Dokümantasyonu#

İçindekiler#

  1. Genel Bakış
  2. Veritabanı Şeması
  3. Flutter Modelleri
  4. Servisler
  5. State Management
  6. UI Bileşenleri
  7. Entegrasyon Noktaları
  8. Kullanım Akışları

Genel Bakış#

Token sistemi, öğretmenlerin öğrenci kağıtlarını yüklemek için kullandıkları kullanım hakkı yönetim sistemidir. Sistem, üç farklı plan tipi ve günlük token limitleri ile çalışır.

Sistem Mimarisi#

┌─────────────────────────────────────────────────────────────┐
│                     Flutter Mobile App                       │
├─────────────────────────────────────────────────────────────┤
│  TokenProvider (State Management)                            │
│  ├─ Token Balance                                           │
│  ├─ Transaction History                                     │
│  └─ Plan Information                                        │
├─────────────────────────────────────────────────────────────┤
│  TokenService (Business Logic)                               │
│  ├─ getUserTokenInfo()                                      │
│  ├─ canUploadPapers()                                       │
│  ├─ deductTokensForPapers()                                 │
│  └─ getTokenHistory()                                       │
├─────────────────────────────────────────────────────────────┤
│  UI Components                                               │
│  ├─ TokenBalanceCard                                        │
│  ├─ InsufficientTokensDialog                                │
│  ├─ UpgradeToSupporterDialog                                │
│  └─ TokenHistoryScreen                                      │
└─────────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                    Supabase Backend                          │
├─────────────────────────────────────────────────────────────┤
│  Database Tables                                             │
│  ├─ token_plans                                             │
│  ├─ user_tokens                                             │
│  └─ token_transactions                                      │
├─────────────────────────────────────────────────────────────┤
│  RPC Functions                                               │
│  ├─ get_user_token_info()                                   │
│  ├─ can_upload_papers()                                     │
│  ├─ deduct_tokens_for_papers()                              │
│  └─ get_token_history()                                     │
├─────────────────────────────────────────────────────────────┤
│  Triggers                                                    │
│  └─ on_user_created_add_tokens                              │
└─────────────────────────────────────────────────────────────┘

Plan Tipleri#

Plan TipiGünlük LimitKümülatifFiyatHedef Kullanıcı
default10 tokenHayır (her gün 10’a sıfırlanır)ÜcretsizStandart öğretmenler
supporterSınırsız-Ücretli (gelecekte)Premium öğretmenler
adminSınırsız--Sistem yöneticileri

Token Maliyeti#

  • 1 fotoğraf = 1 token
  • Sadece öğrenci kağıdı yükleme işlemlerinde token harcanır
  • Diğer işlemler (sınıf oluşturma, sınav oluşturma vb.) ücretsizdir
  • Token sadece öğretmenler için geçerlidir, öğrenciler token kullanmaz

İş Akışı#

  1. Yeni öğretmen kaydı → Otomatik olarak “default” plan atanır
  2. Her gün saat 00:00’da default plan kullanıcılarının tokenları 10’a sıfırlanır
  3. Öğretmen öğrenci kağıdı yüklemek ister → Sistem token kontrolü yapar
  4. Yeterli token varsa → Token düşürülür ve upload işlemi gerçekleşir
  5. Yetersiz token varsa → Kullanıcıya uyarı gösterilir ve “supporter” plana yükseltme seçeneği sunulur

Veritabanı Şeması#

Tablo: token_plans#

Token plan tiplerini tanımlar.

Kolon Yapısı:

CREATE TABLE IF NOT EXISTS token_plans (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  plan_type TEXT NOT NULL UNIQUE CHECK (plan_type IN ('default', 'supporter', 'admin')),
  daily_limit INTEGER NOT NULL,
  is_unlimited BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Kolonlar:

  • id (SERIAL PRIMARY KEY): Benzersiz plan kimliği
  • name (TEXT NOT NULL): Plan adı (örn: “Ücretsiz Plan”, “Destekçi Planı”)
  • plan_type (TEXT NOT NULL UNIQUE): Plan tipi enum değeri
  • daily_limit (INTEGER NOT NULL): Günlük token limiti (-1 = sınırsız)
  • is_unlimited (BOOLEAN): Plan sınırsız mı?
  • created_at (TIMESTAMPTZ): Oluşturulma zamanı

Varsayılan Veriler:

INSERT INTO token_plans (name, plan_type, daily_limit, is_unlimited) VALUES
  ('Ücretsiz Plan', 'default', 10, FALSE),
  ('Destekçi Planı', 'supporter', -1, TRUE),
  ('Admin Planı', 'admin', -1, TRUE);

RLS Politikaları:

  • Herkes okuma yetkisine sahip (SELECT)
  • Admin ve editor’lar yazma yetkisine sahip (INSERT, UPDATE, DELETE)

Tablo: user_tokens#

Kullanıcıların token bakiyelerini ve istatistiklerini saklar.

Kolon Yapısı:

CREATE TABLE IF NOT EXISTS user_tokens (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
  token_balance INTEGER NOT NULL DEFAULT 10,
  last_reset_at TIMESTAMPTZ DEFAULT NOW(),
  lifetime_tokens_spent INTEGER DEFAULT 0,
  total_papers_graded INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Kolonlar:

  • id (SERIAL PRIMARY KEY): Benzersiz kayıt kimliği
  • user_id (INTEGER UNIQUE): Kullanıcı kimliği (foreign key)
  • token_balance (INTEGER): Mevcut token bakiyesi
  • last_reset_at (TIMESTAMPTZ): Son token sıfırlama zamanı
  • lifetime_tokens_spent (INTEGER): Toplam harcanan token sayısı
  • total_papers_graded (INTEGER): Toplam değerlendirilen kağıt sayısı
  • created_at (TIMESTAMPTZ): Kayıt oluşturulma zamanı
  • updated_at (TIMESTAMPTZ): Son güncelleme zamanı

RLS Politikaları:

  • Kullanıcılar sadece kendi token bilgilerini görebilir
  • Sadece authenticated kullanıcılar erişebilir

İndeksler:

CREATE INDEX idx_user_tokens_user_id ON user_tokens(user_id);

Tablo: token_transactions#

Token kullanım geçmişini kaydeder.

Kolon Yapısı:

CREATE TABLE IF NOT EXISTS token_transactions (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  transaction_type TEXT NOT NULL CHECK (transaction_type IN ('deduct', 'add', 'reset')),
  amount INTEGER NOT NULL,
  balance_after INTEGER NOT NULL,
  description TEXT,
  exam_id INTEGER REFERENCES exams(id) ON DELETE SET NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Kolonlar:

  • id (SERIAL PRIMARY KEY): Benzersiz işlem kimliği
  • user_id (INTEGER): Kullanıcı kimliği (foreign key)
  • transaction_type (TEXT): İşlem tipi
    • deduct: Token düşürme (kağıt yükleme)
    • add: Token ekleme (yönetici işlemi)
    • reset: Token sıfırlama (günlük otomatik sıfırlama)
  • amount (INTEGER): İşlem miktarı (pozitif veya negatif)
  • balance_after (INTEGER): İşlem sonrası bakiye
  • description (TEXT): İşlem açıklaması
  • exam_id (INTEGER): İlişkili sınav kimliği (opsiyonel)
  • created_at (TIMESTAMPTZ): İşlem zamanı

RLS Politikaları:

  • Kullanıcılar sadece kendi işlem geçmişlerini görebilir

İndeksler:

CREATE INDEX idx_token_transactions_user_id ON token_transactions(user_id);
CREATE INDEX idx_token_transactions_created_at ON token_transactions(created_at DESC);
CREATE INDEX idx_token_transactions_exam_id ON token_transactions(exam_id);

Kolon Ekleme: users.account_type#

Kullanıcıların plan tipini belirler.

ALTER TABLE users 
ADD COLUMN IF NOT EXISTS account_type TEXT 
REFERENCES token_plans(plan_type) 
DEFAULT 'default';

Varsayılan Değer: 'default'
İlişki: token_plans(plan_type) foreign key


RPC Fonksiyon: get_user_token_info()#

Kullanıcının token bilgilerini, plan detaylarını ve sonraki sıfırlamaya kalan süreyi döndürür.

Parametreler:

  • Yok (mevcut kullanıcı session’dan alınır)

Dönen Veri Yapısı:

{
  "success": true,
  "error": null,
  "plan_type": "default",
  "is_unlimited": false,
  "token_balance": 7,
  "daily_limit": 10,
  "last_reset_at": "2026-01-22T00:00:00Z",
  "hours_until_reset": 12.5
}

İş Akışı:

  1. Kullanıcının role_id‘sini kontrol eder (sadece öğretmenler için)
  2. Kullanıcının plan tipini users.account_type‘dan alır
  3. Plan detaylarını token_plans‘dan okur
  4. Token bakiyesini user_tokens‘dan okur
  5. Sonraki sıfırlamaya kalan süreyi hesaplar
  6. Tüm bilgileri JSON formatında döndürür

SQL Kodu:

CREATE OR REPLACE FUNCTION get_user_token_info()
RETURNS JSON AS $$
DECLARE
  v_user_id INTEGER;
  v_role_id INTEGER;
  v_account_type TEXT;
  v_plan RECORD;
  v_token RECORD;
  v_hours_until_reset NUMERIC;
BEGIN
  -- Get current user ID
  v_user_id := auth.uid()::INTEGER;
  
  IF v_user_id IS NULL THEN
    RETURN json_build_object(
      'success', FALSE,
      'error', 'User not authenticated'
    );
  END IF;
  
  -- Get user role and account type
  SELECT role_id, COALESCE(account_type, 'default') 
  INTO v_role_id, v_account_type
  FROM users 
  WHERE id = v_user_id;
  
  -- Only teachers (role_id = 2) need tokens
  IF v_role_id != 2 THEN
    RETURN json_build_object(
      'success', FALSE,
      'error', 'Token system is only for teachers'
    );
  END IF;
  
  -- Get plan details
  SELECT * INTO v_plan 
  FROM token_plans 
  WHERE plan_type = v_account_type;
  
  -- Get token balance
  SELECT * INTO v_token 
  FROM user_tokens 
  WHERE user_id = v_user_id;
  
  -- Calculate hours until next reset (midnight)
  v_hours_until_reset := EXTRACT(EPOCH FROM (
    (CURRENT_DATE + INTERVAL '1 day')::TIMESTAMPTZ - NOW()
  )) / 3600;
  
  RETURN json_build_object(
    'success', TRUE,
    'error', NULL,
    'plan_type', v_plan.plan_type,
    'is_unlimited', v_plan.is_unlimited,
    'token_balance', COALESCE(v_token.token_balance, 0),
    'daily_limit', v_plan.daily_limit,
    'last_reset_at', v_token.last_reset_at,
    'hours_until_reset', v_hours_until_reset
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

RPC Fonksiyon: can_upload_papers(p_paper_count INTEGER)#

Kullanıcının belirtilen sayıda kağıt yüklemeye yeterli tokene sahip olup olmadığını kontrol eder.

Parametreler:

  • p_paper_count (INTEGER): Yüklenecek kağıt sayısı

Dönen Veri Yapısı:

{
  "success": true,
  "can_upload": true,
  "is_unlimited": false,
  "required": 5,
  "available": 7,
  "shortage": 0
}

İş Akışı:

  1. Kullanıcının plan tipini kontrol eder
  2. Unlimited plan ise → can_upload: true
  3. Default/supporter plan ise → Mevcut bakiye ile gerekli token sayısını karşılaştırır
  4. Eksik token sayısını hesaplar (shortage)
  5. Sonucu JSON formatında döndürür

SQL Kodu:

CREATE OR REPLACE FUNCTION can_upload_papers(p_paper_count INTEGER)
RETURNS JSON AS $$
DECLARE
  v_user_id INTEGER;
  v_token_balance INTEGER;
  v_is_unlimited BOOLEAN;
  v_account_type TEXT;
BEGIN
  v_user_id := auth.uid()::INTEGER;
  
  -- Get account type and check if unlimited
  SELECT 
    u.account_type,
    tp.is_unlimited
  INTO v_account_type, v_is_unlimited
  FROM users u
  JOIN token_plans tp ON tp.plan_type = u.account_type
  WHERE u.id = v_user_id;
  
  -- If unlimited, always return true
  IF v_is_unlimited THEN
    RETURN json_build_object(
      'success', TRUE,
      'can_upload', TRUE,
      'is_unlimited', TRUE,
      'required', p_paper_count,
      'available', -1,
      'shortage', 0
    );
  END IF;
  
  -- Get current token balance
  SELECT token_balance INTO v_token_balance
  FROM user_tokens
  WHERE user_id = v_user_id;
  
  -- Check if enough tokens
  RETURN json_build_object(
    'success', TRUE,
    'can_upload', v_token_balance >= p_paper_count,
    'is_unlimited', FALSE,
    'required', p_paper_count,
    'available', v_token_balance,
    'shortage', GREATEST(0, p_paper_count - v_token_balance)
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

RPC Fonksiyon: deduct_tokens_for_papers(p_paper_count INTEGER, p_exam_id INTEGER)#

Kullanıcının token bakiyesinden belirtilen miktarı düşürür ve işlem kaydı oluşturur.

Parametreler:

  • p_paper_count (INTEGER): Düşürülecek token sayısı
  • p_exam_id (INTEGER, OPTIONAL): İlişkili sınav kimliği

Dönen Veri Yapısı:

{
  "success": true,
  "error": null,
  "is_unlimited": false,
  "tokens_deducted": 5,
  "balance_before": 7,
  "balance_after": 2
}

İş Akışı:

  1. Kullanıcının plan tipini kontrol eder
  2. Unlimited plan ise → İşlem yapmadan başarılı döner
  3. Default/supporter plan ise:
    • Mevcut bakiyeyi kontrol eder
    • Yeterli token yoksa hata döndürür
    • Token bakiyesini düşürür
    • İşlem kaydı oluşturur (token_transactions)
    • İstatistikleri günceller (lifetime_tokens_spent, total_papers_graded)
  4. Sonucu JSON formatında döndürür

SQL Kodu:

CREATE OR REPLACE FUNCTION deduct_tokens_for_papers(
  p_paper_count INTEGER,
  p_exam_id INTEGER DEFAULT NULL
)
RETURNS JSON AS $$
DECLARE
  v_user_id INTEGER;
  v_token_balance INTEGER;
  v_new_balance INTEGER;
  v_is_unlimited BOOLEAN;
  v_account_type TEXT;
BEGIN
  v_user_id := auth.uid()::INTEGER;
  
  -- Get account type and check if unlimited
  SELECT 
    u.account_type,
    tp.is_unlimited
  INTO v_account_type, v_is_unlimited
  FROM users u
  JOIN token_plans tp ON tp.plan_type = u.account_type
  WHERE u.id = v_user_id;
  
  -- If unlimited, no deduction needed
  IF v_is_unlimited THEN
    RETURN json_build_object(
      'success', TRUE,
      'error', NULL,
      'is_unlimited', TRUE,
      'tokens_deducted', 0,
      'balance_before', -1,
      'balance_after', -1
    );
  END IF;
  
  -- Get current balance
  SELECT token_balance INTO v_token_balance
  FROM user_tokens
  WHERE user_id = v_user_id;
  
  -- Check sufficient balance
  IF v_token_balance < p_paper_count THEN
    RETURN json_build_object(
      'success', FALSE,
      'error', 'Insufficient tokens',
      'is_unlimited', FALSE,
      'tokens_deducted', 0,
      'balance_before', v_token_balance,
      'balance_after', v_token_balance
    );
  END IF;
  
  -- Deduct tokens
  v_new_balance := v_token_balance - p_paper_count;
  
  UPDATE user_tokens 
  SET 
    token_balance = v_new_balance,
    lifetime_tokens_spent = lifetime_tokens_spent + p_paper_count,
    total_papers_graded = total_papers_graded + p_paper_count,
    updated_at = NOW()
  WHERE user_id = v_user_id;
  
  -- Record transaction
  INSERT INTO token_transactions (
    user_id, 
    transaction_type, 
    amount, 
    balance_after, 
    description,
    exam_id
  ) VALUES (
    v_user_id,
    'deduct',
    p_paper_count,
    v_new_balance,
    FORMAT('%s kağıt yükleme için token kullanıldı', p_paper_count),
    p_exam_id
  );
  
  RETURN json_build_object(
    'success', TRUE,
    'error', NULL,
    'is_unlimited', FALSE,
    'tokens_deducted', p_paper_count,
    'balance_before', v_token_balance,
    'balance_after', v_new_balance
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

RPC Fonksiyon: get_token_history(p_limit INTEGER, p_offset INTEGER)#

Kullanıcının token işlem geçmişini sayfalama ile getirir.

Parametreler:

  • p_limit (INTEGER, DEFAULT 20): Sayfa başına kayıt sayısı
  • p_offset (INTEGER, DEFAULT 0): Başlangıç offset değeri

Dönen Veri Yapısı:

{
  "success": true,
  "error": null,
  "transactions": [
    {
      "id": 123,
      "transaction_type": "deduct",
      "amount": 5,
      "balance_after": 5,
      "description": "5 kağıt yükleme için token kullanıldı",
      "exam_id": 42,
      "created_at": "2026-01-22T14:30:00Z"
    }
  ],
  "total_count": 150,
  "has_more": true
}

İş Akışı:

  1. Kullanıcının toplam işlem sayısını hesaplar
  2. Belirtilen sayfalama parametreleri ile işlemleri getirir
  3. Tarih sırasına göre (yeniden eskiye) sıralar
  4. Sonucu JSON formatında döndürür

SQL Kodu:

CREATE OR REPLACE FUNCTION get_token_history(
  p_limit INTEGER DEFAULT 20,
  p_offset INTEGER DEFAULT 0
)
RETURNS JSON AS $$
DECLARE
  v_user_id INTEGER;
  v_transactions JSON;
  v_total_count INTEGER;
BEGIN
  v_user_id := auth.uid()::INTEGER;
  
  -- Get total count
  SELECT COUNT(*) INTO v_total_count
  FROM token_transactions
  WHERE user_id = v_user_id;
  
  -- Get transactions with pagination
  SELECT json_agg(row_to_json(t)) INTO v_transactions
  FROM (
    SELECT 
      id,
      transaction_type,
      amount,
      balance_after,
      description,
      exam_id,
      created_at
    FROM token_transactions
    WHERE user_id = v_user_id
    ORDER BY created_at DESC
    LIMIT p_limit
    OFFSET p_offset
  ) t;
  
  RETURN json_build_object(
    'success', TRUE,
    'error', NULL,
    'transactions', COALESCE(v_transactions, '[]'::JSON),
    'total_count', v_total_count,
    'has_more', v_total_count > (p_offset + p_limit)
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Trigger: on_user_created_add_tokens#

Yeni öğretmen kaydı oluşturulduğunda otomatik olarak default plan atanır ve token kaydı oluşturulur.

Tetiklenme Koşulu:

  • users tablosuna yeni kayıt eklenmesi (INSERT)
  • Kullanıcının role_id = 2 (öğretmen) olması

İş Akışı:

  1. Yeni kullanıcının role_id‘sini kontrol eder
  2. Eğer öğretmen ise:
    • account_type‘ı ‘default’ olarak ayarlar (eğer NULL ise)
    • user_tokens tablosuna yeni kayıt oluşturur
    • Başlangıç bakiyesi: 10 token
  3. Trigger sonucu döndürür

SQL Kodu:

CREATE OR REPLACE FUNCTION add_tokens_for_new_user()
RETURNS TRIGGER AS $$
BEGIN
  -- Only for teachers (role_id = 2)
  IF NEW.role_id = 2 THEN
    -- Set default account type if not set
    IF NEW.account_type IS NULL THEN
      NEW.account_type := 'default';
    END IF;
    
    -- Create token record
    INSERT INTO user_tokens (user_id, token_balance, last_reset_at)
    VALUES (NEW.id, 10, NOW())
    ON CONFLICT (user_id) DO NOTHING;
  END IF;
  
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER on_user_created_add_tokens
  BEFORE INSERT ON users
  FOR EACH ROW
  EXECUTE FUNCTION add_tokens_for_new_user();

Flutter Modelleri#

Dosya: lib/common/models/token_plan_model.dart#

Token plan bilgilerini temsil eden model sınıfı.

Sınıf: TokenPlan

class TokenPlan {
  final int id;
  final String name;
  final String planType;
  final int dailyLimit;
  final bool isUnlimited;
  final DateTime createdAt;

  TokenPlan({
    required this.id,
    required this.name,
    required this.planType,
    required this.dailyLimit,
    required this.isUnlimited,
    required this.createdAt,
  });
}

Özellikler:

  • id: Plan benzersiz kimliği
  • name: Plan adı (örn: “Ücretsiz Plan”)
  • planType: Plan tipi (‘default’, ‘supporter’, ‘admin’)
  • dailyLimit: Günlük token limiti (-1 sınırsız için)
  • isUnlimited: Plan sınırsız mı? (boolean)
  • createdAt: Plan oluşturulma tarihi

Metotlar:

factory TokenPlan.fromJson(Map<String, dynamic> json)

  • JSON verisinden TokenPlan nesnesi oluşturur
  • Veritabanından gelen veriyi parse eder
  • Null safety kontrolü yapar

Map<String, dynamic> toJson()

  • TokenPlan nesnesini JSON formatına dönüştürür
  • Veritabanına kaydetmek için kullanılır

String toString()

  • Debug amaçlı string temsili döndürür

Örnek Kullanım:

final plan = TokenPlan.fromJson({
  'id': 1,
  'name': 'Ücretsiz Plan',
  'plan_type': 'default',
  'daily_limit': 10,
  'is_unlimited': false,
  'created_at': '2026-01-01T00:00:00Z',
});

print(plan.isUnlimited); // false
print(plan.dailyLimit); // 10

Dosya: lib/common/models/user_token_model.dart#

Kullanıcı token bilgilerini temsil eden model sınıfları.

Sınıf 1: UserToken

Kullanıcının token bakiyesi ve istatistiklerini temsil eder.

class UserToken {
  final int id;
  final int userId;
  final int tokenBalance;
  final DateTime lastResetAt;
  final int lifetimeTokensSpent;
  final int totalPapersGraded;
  final DateTime createdAt;
  final DateTime updatedAt;

  UserToken({
    required this.id,
    required this.userId,
    required this.tokenBalance,
    required this.lastResetAt,
    this.lifetimeTokensSpent = 0,
    this.totalPapersGraded = 0,
    required this.createdAt,
    required this.updatedAt,
  });
}

Özellikler:

  • id: Kayıt benzersiz kimliği
  • userId: Kullanıcı kimliği
  • tokenBalance: Mevcut token bakiyesi
  • lastResetAt: Son sıfırlama tarihi
  • lifetimeTokensSpent: Toplam harcanan token
  • totalPapersGraded: Toplam değerlendirilen kağıt
  • createdAt: Kayıt oluşturma tarihi
  • updatedAt: Son güncelleme tarihi

Sınıf 2: TokenInfo

RPC get_user_token_info() fonksiyonundan dönen detaylı token bilgisini temsil eder.

class TokenInfo {
  final bool success;
  final String? error;
  final String planType;
  final bool isUnlimited;
  final int tokenBalance;
  final int dailyLimit;
  final DateTime? lastResetAt;
  final double hoursUntilReset;

  TokenInfo({
    required this.success,
    this.error,
    required this.planType,
    required this.isUnlimited,
    required this.tokenBalance,
    required this.dailyLimit,
    this.lastResetAt,
    required this.hoursUntilReset,
  });
}

Özellikler:

  • success: İşlem başarılı mı?
  • error: Hata mesajı (varsa)
  • planType: Kullanıcının plan tipi
  • isUnlimited: Sınırsız plan mı?
  • tokenBalance: Mevcut token bakiyesi
  • dailyLimit: Günlük limit
  • lastResetAt: Son sıfırlama tarihi
  • hoursUntilReset: Sonraki sıfırlamaya kalan saat

Metotlar:

factory TokenInfo.fromJson(Map<String, dynamic> json)

  • JSON verisinden TokenInfo nesnesi oluşturur
  • RPC fonksiyon yanıtını parse eder

String getPlanDisplayName()

  • Plan tipine göre Türkçe plan adı döndürür
  • ‘default’ → “Ücretsiz Plan”
  • ‘supporter’ → “Destekçi Planı”
  • ‘admin’ → “Admin Planı”

String getBalanceDisplayText()

  • Token bakiyesi gösterim metnini döndürür
  • Sınırsız plan: “Sınırsız”
  • Default plan: “7 / 10”

String getResetTimeText()

  • Sıfırlanma süresini okunabilir formatta döndürür
  • Örnek: “12 saat 30 dakika sonra sıfırlanacak”

Örnek Kullanım:

final tokenInfo = TokenInfo.fromJson({
  'success': true,
  'error': null,
  'plan_type': 'default',
  'is_unlimited': false,
  'token_balance': 7,
  'daily_limit': 10,
  'last_reset_at': '2026-01-22T00:00:00Z',
  'hours_until_reset': 12.5,
});

print(tokenInfo.getBalanceDisplayText()); // "7 / 10"
print(tokenInfo.getResetTimeText()); // "12 saat 30 dakika sonra sıfırlanacak"

Sınıf 3: CanUploadResult

Kağıt yükleme yeterlilik kontrolü sonucunu temsil eder.

class CanUploadResult {
  final bool success;
  final bool canUpload;
  final bool isUnlimited;
  final int required;
  final int available;
  final int shortage;

  CanUploadResult({
    required this.success,
    required this.canUpload,
    required this.isUnlimited,
    required this.required,
    required this.available,
    required this.shortage,
  });
}

Özellikler:

  • success: RPC çağrısı başarılı mı?
  • canUpload: Yükleme yapabilir mi?
  • isUnlimited: Sınırsız plan mı?
  • required: Gerekli token sayısı
  • available: Mevcut token sayısı
  • shortage: Eksik token sayısı (0 veya pozitif)

Metotlar:

factory CanUploadResult.fromJson(Map<String, dynamic> json)

  • RPC can_upload_papers() yanıtını parse eder

Örnek Kullanım:

final result = CanUploadResult.fromJson({
  'success': true,
  'can_upload': false,
  'is_unlimited': false,
  'required': 10,
  'available': 7,
  'shortage': 3,
});

if (!result.canUpload) {
  print('${result.shortage} token eksik');
}

Sınıf 4: DeductTokensResult

Token düşürme işlemi sonucunu temsil eder.

class DeductTokensResult {
  final bool success;
  final String? error;
  final bool isUnlimited;
  final int tokensDeducted;
  final int balanceBefore;
  final int balanceAfter;

  DeductTokensResult({
    required this.success,
    this.error,
    required this.isUnlimited,
    required this.tokensDeducted,
    required this.balanceBefore,
    required this.balanceAfter,
  });
}

Özellikler:

  • success: İşlem başarılı mı?
  • error: Hata mesajı (varsa)
  • isUnlimited: Sınırsız plan mı?
  • tokensDeducted: Düşürülen token miktarı
  • balanceBefore: İşlem öncesi bakiye
  • balanceAfter: İşlem sonrası bakiye

Metotlar:

factory DeductTokensResult.fromJson(Map<String, dynamic> json)

  • RPC deduct_tokens_for_papers() yanıtını parse eder

Dosya: lib/common/models/token_transaction_model.dart#

Token işlem geçmişi kayıtlarını temsil eden model sınıfları.

Enum: TokenTransactionType

İşlem tiplerini tanımlar.

enum TokenTransactionType {
  deduct,  // Token düşürme (kağıt yükleme)
  add,     // Token ekleme (yönetici işlemi)
  reset,   // Token sıfırlama (günlük otomatik)
}

Sınıf 1: TokenTransaction

Tek bir token işlemini temsil eder.

class TokenTransaction {
  final int id;
  final int userId;
  final TokenTransactionType transactionType;
  final int amount;
  final int balanceAfter;
  final String? description;
  final int? examId;
  final DateTime createdAt;

  TokenTransaction({
    required this.id,
    required this.userId,
    required this.transactionType,
    required this.amount,
    required this.balanceAfter,
    this.description,
    this.examId,
    required this.createdAt,
  });
}

Özellikler:

  • id: İşlem benzersiz kimliği
  • userId: Kullanıcı kimliği
  • transactionType: İşlem tipi enum
  • amount: İşlem miktarı
  • balanceAfter: İşlem sonrası bakiye
  • description: İşlem açıklaması
  • examId: İlişkili sınav kimliği (opsiyonel)
  • createdAt: İşlem tarihi

Metotlar:

factory TokenTransaction.fromJson(Map<String, dynamic> json)

  • JSON verisinden TokenTransaction nesnesi oluşturur
  • transaction_type string’ini enum’a çevirir

static TokenTransactionType _parseTransactionType(String type)

  • String değeri TokenTransactionType enum’una dönüştürür

String getTypeDisplayText()

  • İşlem tipinin Türkçe karşılığını döndürür
  • ‘deduct’ → “Token Kullanımı”
  • ‘add’ → “Token Eklendi”
  • ‘reset’ → “Token Sıfırlandı”

Color getTypeColor()

  • İşlem tipine göre renk döndürür
  • deduct → Kırmızı
  • add → Yeşil
  • reset → Mavi

IconData getTypeIcon()

  • İşlem tipine göre ikon döndürür
  • deduct → Icons.remove_circle
  • add → Icons.add_circle
  • reset → Icons.refresh

Sınıf 2: TokenHistoryResult

RPC get_token_history() fonksiyonundan dönen sayfalanmış geçmiş sonucunu temsil eder.

class TokenHistoryResult {
  final bool success;
  final String? error;
  final List<TokenTransaction> transactions;
  final int totalCount;
  final bool hasMore;

  TokenHistoryResult({
    required this.success,
    this.error,
    required this.transactions,
    required this.totalCount,
    required this.hasMore,
  });
}

Özellikler:

  • success: İşlem başarılı mı?
  • error: Hata mesajı (varsa)
  • transactions: İşlem listesi
  • totalCount: Toplam işlem sayısı
  • hasMore: Daha fazla kayıt var mı?

Metotlar:

factory TokenHistoryResult.fromJson(Map<String, dynamic> json)

  • RPC yanıtını parse eder
  • İşlem listesini oluşturur

Örnek Kullanım:

final result = TokenHistoryResult.fromJson(apiResponse);

print('Toplam ${result.totalCount} işlem');
for (var transaction in result.transactions) {
  print('${transaction.getTypeDisplayText()}: ${transaction.amount} token');
}

if (result.hasMore) {
  print('Daha fazla kayıt var');
}

Servisler#

Dosya: lib/common/services/token_service.dart#

Token işlemleri için Supabase RPC fonksiyonlarını çağıran servis katmanı.

Sınıf: TokenService

Statik metodlarla token işlemlerini yönetir.


Metod: getUserTokenInfo()

Kullanıcının token bilgilerini getirir.

static Future<TokenInfo> getUserTokenInfo() async {
  try {
    final response = await Supabase.instance.client
        .rpc('get_user_token_info');
    
    print('Token info fetched: ${response}');
    return TokenInfo.fromJson(response as Map<String, dynamic>);
  } catch (e) {
    print('Error fetching token info: $e');
    throw Exception('Token bilgisi alınamadı: $e');
  }
}

Parametreler: Yok
Dönen Değer: Future<TokenInfo>
Hata Yönetimi: Exception fırlatır

Kullanım:

try {
  final tokenInfo = await TokenService.getUserTokenInfo();
  print('Bakiye: ${tokenInfo.tokenBalance}');
} catch (e) {
  print('Hata: $e');
}

Metod: canUploadPapers(int paperCount)

Belirtilen sayıda kağıt yüklemek için yeterli token olup olmadığını kontrol eder.

static Future<CanUploadResult> canUploadPapers(int paperCount) async {
  try {
    final response = await Supabase.instance.client
        .rpc('can_upload_papers', params: {
      'p_paper_count': paperCount,
    });
    
    print('Can upload check: ${response}');
    return CanUploadResult.fromJson(response as Map<String, dynamic>);
  } catch (e) {
    print('Error checking upload permission: $e');
    throw Exception('Yükleme kontrolü yapılamadı: $e');
  }
}

Parametreler:

  • paperCount (int): Yüklenecek kağıt sayısı

Dönen Değer: Future<CanUploadResult>

Kullanım:

final result = await TokenService.canUploadPapers(5);

if (result.canUpload) {
  print('Yükleme yapılabilir');
} else {
  print('${result.shortage} token eksik');
}

Metod: deductTokensForPapers({required int paperCount, int? examId})

Token bakiyesinden belirtilen miktarı düşürür.

static Future<DeductTokensResult> deductTokensForPapers({
  required int paperCount,
  int? examId,
}) async {
  try {
    final params = <String, dynamic>{
      'p_paper_count': paperCount,
    };
    
    if (examId != null) {
      params['p_exam_id'] = examId;
    }
    
    final response = await Supabase.instance.client
        .rpc('deduct_tokens_for_papers', params: params);
    
    print('Tokens deducted: ${response}');
    return DeductTokensResult.fromJson(response as Map<String, dynamic>);
  } catch (e) {
    print('Error deducting tokens: $e');
    throw Exception('Token düşürme hatası: $e');
  }
}

Parametreler:

  • paperCount (int, required): Düşürülecek token miktarı
  • examId (int?, optional): İlişkili sınav kimliği

Dönen Değer: Future<DeductTokensResult>

Kullanım:

final result = await TokenService.deductTokensForPapers(
  paperCount: 5,
  examId: 42,
);

if (result.success) {
  print('${result.tokensDeducted} token düşürüldü');
  print('Yeni bakiye: ${result.balanceAfter}');
}

Metod: getTokenHistory({int limit = 20, int offset = 0})

Kullanıcının token işlem geçmişini sayfalama ile getirir.

static Future<TokenHistoryResult> getTokenHistory({
  int limit = 20,
  int offset = 0,
}) async {
  try {
    final response = await Supabase.instance.client
        .rpc('get_token_history', params: {
      'p_limit': limit,
      'p_offset': offset,
    });
    
    print('Token history fetched: ${response}');
    return TokenHistoryResult.fromJson(response as Map<String, dynamic>);
  } catch (e) {
    print('Error fetching token history: $e');
    throw Exception('Token geçmişi alınamadı: $e');
  }
}

Parametreler:

  • limit (int, default: 20): Sayfa başına kayıt sayısı
  • offset (int, default: 0): Başlangıç offset değeri

Dönen Değer: Future<TokenHistoryResult>

Kullanım:

// İlk 20 kayıt
final result1 = await TokenService.getTokenHistory();

// Sonraki 20 kayıt
final result2 = await TokenService.getTokenHistory(offset: 20);

print('Toplam ${result1.totalCount} işlem');

State Management#

Dosya: lib/common/core/providers/token_provider.dart#

Token state yönetimi için Provider (ChangeNotifier) implementasyonu.

Sınıf: TokenProvider extends ChangeNotifier

Uygulama genelinde token durumunu yönetir ve UI’a bildirim gönderir.


Özellikler (Properties):

class TokenProvider extends ChangeNotifier {
  TokenInfo? _tokenInfo;
  TokenPlan? _userPlan;
  List<TokenTransaction> _transactionHistory = [];
  
  bool _isLoading = false;
  bool _isLoadingHistory = false;
  
  int _currentHistoryOffset = 0;
  bool _hasMoreHistory = false;
  int _totalHistoryCount = 0;
  
  // Getters
  TokenInfo? get tokenInfo => _tokenInfo;
  TokenPlan? get userPlan => _userPlan;
  List<TokenTransaction> get transactionHistory => _transactionHistory;
  bool get isLoading => _isLoading;
  bool get isLoadingHistory => _isLoadingHistory;
  bool get hasMoreHistory => _hasMoreHistory;
  int get totalHistoryCount => _totalHistoryCount;
}

State Değişkenleri:

  • _tokenInfo: Mevcut token bilgisi
  • _userPlan: Kullanıcının aktif planı
  • _transactionHistory: İşlem geçmişi listesi
  • _isLoading: Ana yükleme durumu
  • _isLoadingHistory: Geçmiş yükleme durumu
  • _currentHistoryOffset: Sayfalama offset’i
  • _hasMoreHistory: Daha fazla kayıt var mı?
  • _totalHistoryCount: Toplam işlem sayısı

Metod: initialize()

Provider’ı başlatır ve token bilgilerini yükler.

Future<void> initialize() async {
  await refreshTokenInfo();
  await _loadUserPlan();
}

Ne Yapar:

  1. Token bilgilerini yeniler
  2. Kullanıcının planını yükler

Kullanım:

// Login sonrası
await tokenProvider.initialize();

Metod: refreshTokenInfo()

Token bilgilerini sunucudan yeniler.

Future<void> refreshTokenInfo() async {
  try {
    _isLoading = true;
    notifyListeners();
    
    _tokenInfo = await TokenService.getUserTokenInfo();
    
    print('Token info refreshed: $_tokenInfo');
    
    _isLoading = false;
    notifyListeners();
  } catch (e) {
    print('Error refreshing token info: $e');
    _isLoading = false;
    notifyListeners();
  }
}

Ne Yapar:

  1. Yükleme durumunu aktif eder
  2. TokenService’den güncel bilgiyi çeker
  3. State’i günceller ve listeners’a bildirir

Kullanım:

// Upload sonrası bakiye güncellemesi
await tokenProvider.refreshTokenInfo();

Metod: canUploadPapers(int paperCount)

Belirtilen sayıda kağıt yüklemek için yeterli token kontrolü yapar.

Future<CanUploadResult> canUploadPapers(int paperCount) async {
  try {
    final result = await TokenService.canUploadPapers(paperCount);
    return result;
  } catch (e) {
    print('Error checking upload permission: $e');
    return CanUploadResult(
      success: false,
      canUpload: false,
      isUnlimited: false,
      required: paperCount,
      available: 0,
      shortage: paperCount,
    );
  }
}

Parametreler:

  • paperCount (int): Kontrol edilecek kağıt sayısı

Dönen Değer: Future<CanUploadResult>

Kullanım:

final result = await tokenProvider.canUploadPapers(5);

if (!result.canUpload) {
  // Yetersiz token dialog göster
}

Metod: deductTokensForPapers({required int paperCount, int? examId})

Token düşürme işlemini gerçekleştirir ve bakiyeyi günceller.

Future<DeductTokensResult> deductTokensForPapers({
  required int paperCount,
  int? examId,
}) async {
  try {
    final result = await TokenService.deductTokensForPapers(
      paperCount: paperCount,
      examId: examId,
    );
    
    // Refresh token info after deduction
    if (result.success) {
      await refreshTokenInfo();
    }
    
    return result;
  } catch (e) {
    print('Error deducting tokens: $e');
    return DeductTokensResult(
      success: false,
      error: e.toString(),
      isUnlimited: false,
      tokensDeducted: 0,
      balanceBefore: 0,
      balanceAfter: 0,
    );
  }
}

Parametreler:

  • paperCount (int, required): Düşürülecek token miktarı
  • examId (int?, optional): Sınav kimliği

Dönen Değer: Future<DeductTokensResult>

Ne Yapar:

  1. Token düşürme işlemini gerçekleştirir
  2. İşlem başarılıysa token bilgilerini yeniler
  3. Sonucu döndürür

Kullanım:

final result = await tokenProvider.deductTokensForPapers(
  paperCount: 5,
  examId: examId,
);

if (result.success) {
  print('İşlem başarılı, yeni bakiye: ${result.balanceAfter}');
}

Metod: loadTransactionHistory({bool refresh = false})

Token işlem geçmişini yükler (sayfalama destekli).

Future<void> loadTransactionHistory({bool refresh = false}) async {
  if (_isLoadingHistory) return;
  
  // Reset pagination on refresh
  if (refresh) {
    _currentHistoryOffset = 0;
    _transactionHistory.clear();
  }
  
  try {
    _isLoadingHistory = true;
    notifyListeners();
    
    final result = await TokenService.getTokenHistory(
      limit: 20,
      offset: _currentHistoryOffset,
    );
    
    print('Loaded ${result.transactions.length} transactions');
    
    _transactionHistory.addAll(result.transactions);
    _totalHistoryCount = result.totalCount;
    _hasMoreHistory = result.hasMore;
    _currentHistoryOffset += result.transactions.length;
    
    _isLoadingHistory = false;
    notifyListeners();
  } catch (e) {
    print('Error loading transaction history: $e');
    _isLoadingHistory = false;
    notifyListeners();
  }
}

Parametreler:

  • refresh (bool, default: false): True ise liste sıfırlanır

Ne Yapar:

  1. Refresh modunda listeyi sıfırlar
  2. Sunucudan 20 kayıt çeker
  3. Mevcut listeye ekler
  4. Sayfalama state’ini günceller

Kullanım:

// İlk yükleme
await tokenProvider.loadTransactionHistory(refresh: true);

// Daha fazla yükleme (scroll)
if (tokenProvider.hasMoreHistory) {
  await tokenProvider.loadTransactionHistory();
}

Metod: _loadUserPlan()

Kullanıcının aktif planını yükler (private).

Future<void> _loadUserPlan() async {
  try {
    final userId = Supabase.instance.client.auth.currentUser?.id;
    if (userId == null) return;
    
    _userPlan = await TokenService.getUserPlan(int.parse(userId));
    print('User plan loaded: $_userPlan');
    notifyListeners();
  } catch (e) {
    print('Error loading user plan: $e');
  }
}

Metod: reset()

Provider state’ini sıfırlar (logout için).

void reset() {
  _tokenInfo = null;
  _userPlan = null;
  _transactionHistory.clear();
  _currentHistoryOffset = 0;
  _hasMoreHistory = false;
  _totalHistoryCount = 0;
  _isLoading = false;
  _isLoadingHistory = false;
  notifyListeners();
}

Kullanım:

// Logout işlemi
await AuthService.logout();
tokenProvider.reset();

UI Bileşenleri#

Dosya: lib/common/widgets/token_balance_card.dart#

Token bakiyesini ve plan bilgilerini gösteren kart widget’ı.

Widget: TokenBalanceCard

class TokenBalanceCard extends StatelessWidget {
  final VoidCallback? onHistoryTap;
  final VoidCallback? onUpgradeTap;

  const TokenBalanceCard({
    super.key,
    this.onHistoryTap,
    this.onUpgradeTap,
  });
}

Parametreler:

  • onHistoryTap (VoidCallback?): Geçmiş butonu callback’i
  • onUpgradeTap (VoidCallback?): Yükseltme butonu callback’i

Görsel Özellikler:

Plan Tipine Göre Renkler:

  • Default Plan: Mavi gradient (#4A7FC7#3164B0)
  • Supporter Plan: Altın gradient (#FFD700#FFA500)
  • Admin Plan: Mor gradient (#9B59B6#8E44AD)

İçerik:

  1. Plan adı ve rozet ikonu
  2. Token bakiyesi (büyük font)
  3. Progress bar (sadece default plan için)
  4. Sıfırlanma zamanı (sadece default plan için)
  5. İşlem geçmişi butonu
  6. Yükseltme butonu (sadece default plan için)

Örnek Kullanım:

TokenBalanceCard(
  onHistoryTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const TokenHistoryScreen(),
      ),
    );
  },
  onUpgradeTap: () {
    UpgradeToSupporterDialog.show(context);
  },
)

Görünüm Örneği:

┌─────────────────────────────────────┐
│         Ücretsiz Plan 🎟️           │
│                                     │
│             7 / 10                  │
│         [========   ]               │
│   12 saat 30 dakika sonra           │
│                                     │
│  [İşlem Geçmişi]  [Yükselt]        │
└─────────────────────────────────────┘

Dosya: lib/common/widgets/insufficient_tokens_dialog.dart#

Yetersiz token durumunda gösterilen uyarı dialog’u.

Widget: InsufficientTokensDialog

class InsufficientTokensDialog extends StatelessWidget {
  final int required;
  final int available;
  final int shortage;
  final VoidCallback? onUpgradeTap;

  const InsufficientTokensDialog({
    super.key,
    required this.required,
    required this.available,
    required this.shortage,
    this.onUpgradeTap,
  });
}

Parametreler:

  • required (int): Gerekli token sayısı
  • available (int): Mevcut token sayısı
  • shortage (int): Eksik token sayısı
  • onUpgradeTap (VoidCallback?): Yükseltme butonu callback’i

Statik Metod: show()

static Future<bool?> show(
  BuildContext context, {
  required int required,
  required int available,
  VoidCallback? onUpgradeTap,
}) {
  final shortage = required - available;
  
  return showDialog<bool>(
    context: context,
    builder: (context) => InsufficientTokensDialog(
      required: required,
      available: available,
      shortage: shortage,
      onUpgradeTap: onUpgradeTap,
    ),
  );
}

Dönen Değer: Future<bool?> (true: devam et, false/null: iptal)

İçerik:

  1. Uyarı başlığı ve ikonu
  2. Gerekli/mevcut/eksik token bilgisi
  3. İptal butonu
  4. Yükseltme butonu

Örnek Kullanım:

final result = await InsufficientTokensDialog.show(
  context,
  required: 10,
  available: 7,
  onUpgradeTap: () {
    UpgradeToSupporterDialog.show(context);
  },
);

if (result != true) {
  // Kullanıcı iptal etti
  return;
}

Görünüm Örneği:

┌─────────────────────────────────────┐
│          Yetersiz Token             │
│              ⚠️                     │
│                                     │
│  Gerekli Token:    10               │
│  Mevcut Token:      7               │
│  Eksik Token:       3               │
│                                     │
│  [İptal]     [Destekçi Ol]         │
└─────────────────────────────────────┘

Dosya: lib/common/widgets/upgrade_to_supporter_dialog.dart#

Supporter plan tanıtımı ve yükseltme dialog’u.

Widget: UpgradeToSupporterDialog

class UpgradeToSupporterDialog extends StatelessWidget {
  const UpgradeToSupporterDialog({super.key});
}

Statik Metod: show()

static Future<void> show(BuildContext context) {
  return showDialog(
    context: context,
    builder: (context) => const UpgradeToSupporterDialog(),
  );
}

İçerik:

  1. “Destekçi Planı” başlığı
  2. Plan özellikleri listesi:
    • Sınırsız token
    • Tüm özelliklere erişim
    • Öncelikli destek
    • Gelecek özelliklere erken erişim
  3. “Yakında” uyarısı
  4. Kapat butonu

Örnek Kullanım:

UpgradeToSupporterDialog.show(context);

Görünüm Örneği:

┌─────────────────────────────────────┐
│        Destekçi Planı               │
│                                     │
│  ✓ Sınırsız token                   │
│  ✓ Tüm özelliklere erişim           │
│  ✓ Öncelikli destek                 │
│  ✓ Gelecek özelliklere erken        │
│    erişim                           │
│                                     │
│  ℹ️ Yakında: Ödeme entegrasyonu     │
│     gelecek                         │
│                                     │
│           [Kapat]                   │
└─────────────────────────────────────┘

Dosya: lib/common/pages/token_history_screen.dart#

Token işlem geçmişini liste halinde gösteren tam sayfa ekranı.

Widget: TokenHistoryScreen

class TokenHistoryScreen extends StatefulWidget {
  const TokenHistoryScreen({super.key});

  @override
  State<TokenHistoryScreen> createState() => _TokenHistoryScreenState();
}

State: _TokenHistoryScreenState

Özellikler:

  • Scroll controller ile sayfalama
  • Pull-to-refresh desteği
  • Tarih gruplandırması
  • Sonsuz scroll yükleme

Lifecycle Metodları:

initState()

@override
void initState() {
  super.initState();
  _scrollController.addListener(_onScroll);
  // Build sonrası history yükle
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _loadHistory();
  });
}

_loadHistory()

void _loadHistory() {
  final provider = Provider.of<TokenProvider>(context, listen: false);
  provider.loadTransactionHistory(refresh: true);
}

_onScroll()

void _onScroll() {
  // Sayfanın %80'ine ulaşıldığında daha fazla yükle
  if (_scrollController.position.pixels >= 
      _scrollController.position.maxScrollExtent - 200) {
    final provider = Provider.of<TokenProvider>(context, listen: false);
    if (!provider.isLoadingHistory && provider.hasMoreHistory) {
      provider.loadTransactionHistory();
    }
  }
}

UI Bileşenleri:

1. AppBar

  • Başlık: “Token Geçmişi”
  • Geri butonu

2. Token Özeti Kartı

  • Mevcut bakiye
  • Toplam harcanan token
  • Toplam işlem sayısı

3. İşlem Listesi

  • Tarih gruplandırması
  • İşlem tipi ikonu ve rengi
  • İşlem açıklaması
  • Token miktarı (+/-)
  • İşlem sonrası bakiye

4. Yükleme Göstergeleri

  • Liste başında: CircularProgressIndicator
  • Liste sonunda: Mini loading indicator

Örnek Kullanım:

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const TokenHistoryScreen(),
  ),
);

Görünüm Örneği:

┌─────────────────────────────────────┐
│  ← Token Geçmişi                    │
├─────────────────────────────────────┤
│  Mevcut Bakiye:         7 token     │
│  Toplam Harcanan:      43 token     │
│  Toplam İşlem:         15           │
├─────────────────────────────────────┤
│  22 Ocak 2026                       │
│  ├─ 🔴 Token Kullanımı              │
│  │   5 kağıt yükleme     -5         │
│  │   Bakiye: 7                      │
│  │   14:30                          │
│  │                                  │
│  └─ 🔵 Token Sıfırlandı             │
│      Günlük sıfırlama    +10        │
│      Bakiye: 10                     │
│      00:00                          │
├─────────────────────────────────────┤
│  21 Ocak 2026                       │
│  └─ 🔴 Token Kullanımı              │
│      3 kağıt yükleme     -3         │
│      Bakiye: 7                      │
│      16:45                          │
└─────────────────────────────────────┘

Entegrasyon Noktaları#

1. Dosya: lib/main.dart#

TokenProvider Eklenmesi:

MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => ThemeProvider()),
    ChangeNotifierProvider(create: (_) => TokenProvider()),
  ],
  child: MyApp(),
)

İşlev: TokenProvider’ı uygulama genelinde erişilebilir yapar.


AuthWrapper’da Başlatma:

class _AuthWrapperState extends State<AuthWrapper> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    
    if (_session != null) {
      // Initialize TokenProvider for logged-in users
      WidgetsBinding.instance.addPostFrameCallback((_) {
        final tokenProvider = Provider.of<TokenProvider>(
          context, 
          listen: false,
        );
        tokenProvider.initialize();
      });
    }
  }
}

İşlev: Kullanıcı giriş yaptığında TokenProvider’ı başlatır.


2. Dosya: lib/common/pages/login_screen.dart#

Login Sonrası TokenProvider Başlatma:

Future<void> _handleLogin() async {
  // ... login logic ...
  
  if (mounted) {
    // Initialize TokenProvider after successful login
    final tokenProvider = Provider.of<TokenProvider>(
      context,
      listen: false,
    );
    await tokenProvider.initialize();
    print('DEBUG AUTH: TokenProvider initialized');
    
    // Navigate to dashboard
    Navigator.pushReplacement(context, ...);
  }
}

İşlev: Login başarılı olduğunda token bilgilerini yükler.


3. Dosya: lib/teacher/pages/profile/teacher_profile_screen.dart#

TokenBalanceCard Entegrasyonu:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SingleChildScrollView(
      child: Column(
        children: [
          // Kullanıcı bilgileri
          _buildUserInfoCard(),
          
          // Token bakiye kartı
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TokenBalanceCard(
              onHistoryTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const TokenHistoryScreen(),
                  ),
                );
              },
              onUpgradeTap: () {
                UpgradeToSupporterDialog.show(context);
              },
            ),
          ),
          
          // Diğer profil öğeleri
          _buildProfileOptions(),
        ],
      ),
    ),
  );
}

İşlev: Öğretmen profil sayfasında token bakiyesini gösterir.


Logout’ta TokenProvider Sıfırlama:

void _handleLogout() async {
  await AuthService.logout();
  
  // Reset TokenProvider
  if (context.mounted) {
    Provider.of<TokenProvider>(context, listen: false).reset();
  }
  
  // Navigate to login
  Navigator.pushReplacement(context, ...);
}

İşlev: Çıkış yapıldığında token state’ini temizler.


4. Dosya: lib/teacher/pages/home/teacher_dashboard_screen.dart#

Header’da Token Badge Eklenmesi:

Widget _buildHeader(BuildContext context) {
  return Container(
    padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
    child: Row(
      children: [
        // Avatar
        CircleAvatar(...),
        
        const SizedBox(width: 12),
        
        // Kullanıcı bilgileri
        Expanded(child: ...),
        
        // Token badge
        _buildTokenBadge(context),
        const SizedBox(width: 8),
        
        // Logout butonu
        IconButton(...),
      ],
    ),
  );
}

_buildTokenBadge() Widget’ı:

Widget _buildTokenBadge(BuildContext context) {
  return Consumer<TokenProvider>(
    builder: (context, tokenProvider, child) {
      final tokenInfo = tokenProvider.tokenInfo;
      
      if (tokenProvider.isLoading || tokenInfo == null) {
        return CircularProgressIndicator();
      }
      
      // Plan tipine göre badge rengi ve içeriği
      Color badgeColor;
      Color textColor;
      String displayText;
      IconData badgeIcon;
      
      if (tokenInfo.planType == 'admin') {
        badgeColor = const Color(0xFF9B59B6).withValues(alpha: 0.2);
        textColor = const Color(0xFF9B59B6);
        displayText = '∞';
        badgeIcon = Icons.admin_panel_settings;
      } else if (tokenInfo.isUnlimited) {
        badgeColor = const Color(0xFFF39C12).withValues(alpha: 0.2);
        textColor = const Color(0xFFF39C12);
        displayText = '∞';
        badgeIcon = Icons.star;
      } else {
        // Default plan - token durumuna göre renk
        final balance = tokenInfo.tokenBalance;
        final limit = tokenInfo.dailyLimit;
        final ratio = limit > 0 ? balance / limit : 1.0;
        
        if (ratio > 0.5) {
          badgeColor = AppColors.primary.withValues(alpha: 0.15);
          textColor = AppColors.primary;
        } else if (ratio > 0.2) {
          badgeColor = Colors.orange.withValues(alpha: 0.15);
          textColor = Colors.orange;
        } else {
          badgeColor = Colors.red.withValues(alpha: 0.15);
          textColor = Colors.red;
        }
        displayText = '$balance';
        badgeIcon = Icons.token;
      }
      
      return Container(
        padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
        decoration: BoxDecoration(
          color: badgeColor,
          borderRadius: BorderRadius.circular(16),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(badgeIcon, size: 14, color: textColor),
            const SizedBox(width: 4),
            Text(
              displayText,
              style: TextStyle(
                fontSize: 12,
                fontWeight: FontWeight.w600,
                color: textColor,
              ),
            ),
          ],
        ),
      );
    },
  );
}

İşlev: Dashboard header’ında kompakt token göstergesi sağlar.


Logout’ta TokenProvider Sıfırlama:

void _handleLogout() async {
  await AuthService.logout();
  
  // Reset TokenProvider state
  if (context.mounted) {
    Provider.of<TokenProvider>(context, listen: false).reset();
  }
  
  Navigator.pushAndRemoveUntil(context, ...);
}

5. Dosya: lib/teacher/pages/exams/exam_student_list_screen.dart#

İmportlar:

import 'package:provider/provider.dart';
import '../../../common/core/providers/token_provider.dart';
import '../../../common/widgets/insufficient_tokens_dialog.dart';
import '../../../common/widgets/upgrade_to_supporter_dialog.dart';

Tekli Upload - _handleSendSingleStudent() Fonksiyonu:

Future<void> _handleSendSingleStudent(
  BuildContext context,
  StudentListItem student,
  List<File> files,
) async {
  // Token kontrolü
  final photoCount = files.length;
  final tokenProvider = Provider.of<TokenProvider>(context, listen: false);
  
  // 1. Token yeterliliği kontrolü
  final canUploadResult = await tokenProvider.canUploadPapers(photoCount);
  
  if (!canUploadResult.canUpload && !canUploadResult.isUnlimited) {
    // Yetersiz token - dialog göster
    if (context.mounted) {
      final result = await InsufficientTokensDialog.show(
        context,
        required: canUploadResult.required,
        available: canUploadResult.available,
        onUpgradeTap: () => UpgradeToSupporterDialog.show(context),
      );
      
      if (result != true) {
        return; // Kullanıcı iptal etti
      }
    }
    return;
  }
  
  // 2. Token düşürme işlemi
  final deductResult = await tokenProvider.deductTokensForPapers(
    paperCount: photoCount,
    examId: int.tryParse(widget.examId),
  );
  
  if (!deductResult.success && !deductResult.isUnlimited) {
    // Token düşürme hatası
    if (context.mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Token düşürme hatası: ${deductResult.error}'),
          backgroundColor: Colors.red,
        ),
      );
    }
    return;
  }
  
  // 3. Upload işlemi
  setState(() {
    _uploadingStudentIds.add(student.id);
  });
  
  try {
    final success = await _uploadStudentPaper(student, files);
    
    if (success) {
      // Başarılı - bakiyeyi yenile ve göster
      await tokenProvider.refreshTokenInfo();
      final tokenInfo = tokenProvider.tokenInfo;
      final tokenText = tokenInfo?.isUnlimited == true
          ? '(Sınırsız)'
          : '(Kalan: ${tokenInfo?.tokenBalance ?? 0} token)';
      
      if (context.mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              'Kağıt başarıyla yüklendi ${student.studentName} $tokenText',
            ),
            backgroundColor: Colors.green,
          ),
        );
      }
      
      setState(() {
        _uploadedStudentIds.add(student.id);
      });
    }
  } finally {
    setState(() {
      _uploadingStudentIds.remove(student.id);
    });
  }
}

İş Akışı:

  1. Fotoğraf sayısını hesapla
  2. Token yeterliliği kontrol et
  3. Yetersizse dialog göster
  4. Yeterliyse token düş
  5. Upload işlemini gerçekleştir
  6. Başarılıysa bakiyeyi yenile ve göster

Toplu Upload - _handleProcessExams() Fonksiyonu:

Future<void> _handleProcessExams() async {
  if (_studentPaperFiles.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('En az bir öğrenci kağıdı yüklemelisiniz'),
        backgroundColor: Colors.red,
      ),
    );
    return;
  }
  
  // 1. Toplam fotoğraf sayısını hesapla
  int totalPhotoCount = 0;
  for (final files in _studentPaperFiles.values) {
    totalPhotoCount += files.length;
  }
  
  // 2. Token kontrolü
  final tokenProvider = Provider.of<TokenProvider>(context, listen: false);
  final canUploadResult = await tokenProvider.canUploadPapers(totalPhotoCount);
  
  if (!canUploadResult.canUpload && !canUploadResult.isUnlimited) {
    // Yetersiz token
    if (mounted) {
      final result = await InsufficientTokensDialog.show(
        context,
        required: canUploadResult.required,
        available: canUploadResult.available,
        onUpgradeTap: () => UpgradeToSupporterDialog.show(context),
      );
      
      if (result != true || canUploadResult.available == 0) {
        return;
      }
      
      // Uyarı göster - toplu upload için token yetersiz
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            '$totalPhotoCount fotoğraf için yeterli token yok. '
            'Daha az fotoğraf yükleyin.',
          ),
          backgroundColor: Colors.orange,
          duration: const Duration(seconds: 3),
        ),
      );
      return;
    }
    return;
  }
  
  // 3. Token düş (upload'tan ÖNCE - toplu olarak)
  final deductResult = await tokenProvider.deductTokensForPapers(
    paperCount: totalPhotoCount,
    examId: int.tryParse(widget.examId),
  );
  
  if (!deductResult.success && !deductResult.isUnlimited) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Token düşürme hatası: ${deductResult.error}'),
          backgroundColor: Colors.red,
        ),
      );
    }
    return;
  }
  
  // 4. Upload işlemi
  setState(() {
    _isProcessing = true;
  });
  
  try {
    int successCount = 0;
    int failCount = 0;
    
    // Her öğrenci için upload
    for (final entry in _studentPaperFiles.entries) {
      final studentId = entry.key;
      final files = entry.value;
      
      // Zaten yüklenmiş öğrencileri atla
      if (_uploadedStudentIds.contains(studentId)) {
        continue;
      }
      
      final student = _students.firstWhere((s) => s.id == studentId);
      final success = await _uploadStudentPaper(student, files);
      
      if (success) {
        successCount++;
        _uploadedStudentIds.add(studentId);
      } else {
        failCount++;
      }
    }
    
    setState(() {
      _isProcessing = false;
    });
    
    // 5. Token bilgisini yenile ve sonuç göster
    await tokenProvider.refreshTokenInfo();
    final tokenInfo = tokenProvider.tokenInfo;
    final tokenText = tokenInfo?.isUnlimited == true
        ? '(Sınırsız)'
        : '(Kalan: ${tokenInfo?.tokenBalance ?? 0} token)';
    
    if (failCount == 0) {
      // Tümü başarılı
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              'Tüm sınav kağıtları başarıyla yüklendi! '
              '($successCount kağıt) $tokenText',
            ),
            backgroundColor: AppColors.cardGreen,
            duration: const Duration(seconds: 3),
          ),
        );
        
        Navigator.pop(context, true);
      }
    } else if (successCount > 0) {
      // Kısmi başarı
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              '$successCount kağıt yüklendi, $failCount kağıt başarısız oldu '
              '$tokenText',
            ),
            backgroundColor: Colors.orange,
            duration: const Duration(seconds: 4),
          ),
        );
      }
    } else {
      // Tümü başarısız
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text(
              'Hiçbir kağıt yüklenemedi ($failCount başarısız)',
            ),
            backgroundColor: Colors.red,
            duration: const Duration(seconds: 4),
          ),
        );
      }
    }
  } catch (e) {
    setState(() {
      _isProcessing = false;
    });
    
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Hata: $e'),
          backgroundColor: Colors.red,
        ),
      );
    }
  }
}

İş Akışı:

  1. Toplam fotoğraf sayısını hesapla
  2. Token yeterliliği kontrol et
  3. Yetersizse uyarı göster ve çık
  4. Yeterliyse tüm tokenları bir kerede düş
  5. Her öğrenci için upload yap
  6. Sonuçları topla
  7. Bakiyeyi yenile ve sonuç mesajı göster

6. Dosya: lib/student/pages/student_profile_screen.dart#

Logout’ta TokenProvider Sıfırlama:

void _handleLogout() async {
  await AuthService.logout();
  
  // Reset TokenProvider
  if (context.mounted) {
    Provider.of<TokenProvider>(context, listen: false).reset();
  }
  
  Navigator.pushReplacement(context, ...);
}

Not: Öğrencilerin token sistemi yok, ancak logout sırasında state temizliği için reset çağrılır.


Kullanım Akışları#

Akış 1: Yeni Öğretmen Kaydı#

1. Öğretmen kayıt formunu doldurur
   └─> POST /auth/signup
   
2. Supabase users tablosuna kayıt eklenir
   └─> Trigger: on_user_created_add_tokens çalışır
   
3. Trigger otomatik olarak:
   - account_type = 'default' atar
   - user_tokens tablosuna kayıt oluşturur
   - Başlangıç bakiyesi: 10 token
   
4. Login ekranına yönlendirilir

Akış 2: Login ve Token Bilgisi Yükleme#

1. Öğretmen giriş yapar
   └─> AuthService.login()
   
2. Login başarılı olunca:
   └─> TokenProvider.initialize() çağrılır
   
3. TokenProvider.initialize():
   └─> refreshTokenInfo()
       └─> RPC: get_user_token_info()
           └─> TokenInfo nesnesi oluşturulur
   └─> _loadUserPlan()
       └─> TokenService.getUserPlan()
           └─> TokenPlan nesnesi oluşturulur
   
4. Dashboard açılır
   └─> Header'da token badge gösterilir
   └─> Profil sayfasında TokenBalanceCard gösterilir

Akış 3: Tekli Kağıt Yükleme#

1. Öğretmen öğrenci listesinde bir öğrenci seçer
   └─> Fotoğraf seçer (örn: 5 fotoğraf)
   
2. "Gönder" butonuna basar
   └─> _handleSendSingleStudent() çağrılır
   
3. Token kontrolü:
   └─> tokenProvider.canUploadPapers(5)
       └─> RPC: can_upload_papers(5)
           └─> Result: canUpload = true/false
   
4a. Yetersiz token durumunda:
    └─> InsufficientTokensDialog gösterilir
        ├─> Kullanıcı "İptal" yaparsa → İşlem durur
        └─> Kullanıcı "Destekçi Ol" yaparsa
            └─> UpgradeToSupporterDialog gösterilir
   
4b. Yeterli token durumunda:
    └─> tokenProvider.deductTokensForPapers(5)
        └─> RPC: deduct_tokens_for_papers(5)
            ├─> user_tokens tablosunda bakiye güncellenir
            ├─> token_transactions tablosuna kayıt eklenir
            └─> Result: balanceAfter = 5
    
5. Token düşürme başarılı:
   └─> Upload işlemi başlar
       └─> FileUploadService.uploadPaper()
   
6. Upload başarılı:
   └─> tokenProvider.refreshTokenInfo()
   └─> Başarı mesajı: "Kağıt yüklendi (Kalan: 5 token)"

Akış 4: Toplu Kağıt Yükleme#

1. Öğretmen birden fazla öğrenci için fotoğraf seçer
   └─> Örn: 3 öğrenci, toplam 15 fotoğraf
   
2. "Tümünü Yükle" butonuna basar
   └─> _handleProcessExams() çağrılır
   
3. Toplam fotoğraf sayısı hesaplanır: 15
   
4. Token kontrolü:
   └─> tokenProvider.canUploadPapers(15)
       └─> RPC: can_upload_papers(15)
   
5a. Yetersiz token (örn: 10 mevcut):
    └─> InsufficientTokensDialog
        └─> "15 fotoğraf için yeterli token yok. 
             Daha az fotoğraf yükleyin."
        └─> İşlem iptal edilir
   
5b. Yeterli token:
    └─> tokenProvider.deductTokensForPapers(15)
        └─> RPC: deduct_tokens_for_papers(15)
            └─> Tüm 15 token bir kerede düşürülür
    
6. Upload işlemi başlar:
   └─> Her öğrenci için sırayla:
       ├─> Öğrenci 1: 5 fotoğraf → Upload
       ├─> Öğrenci 2: 4 fotoğraf → Upload
       └─> Öğrenci 3: 6 fotoğraf → Upload
   
7. Sonuç gösterilir:
   └─> tokenProvider.refreshTokenInfo()
   └─> "Tüm kağıtlar yüklendi (3 öğrenci) (Kalan: 0 token)"

Akış 5: Token Sıfırlanması (Günlük Otomatik)#

1. Sistem saati 00:00'a gelir
   
2. Cron job veya scheduled task çalışır
   └─> Tüm default plan kullanıcıları için:
       
3. Her kullanıcı için:
   └─> UPDATE user_tokens
       SET token_balance = 10,
           last_reset_at = NOW()
       WHERE user_id = ? AND account_type = 'default'
   
   └─> INSERT INTO token_transactions
       (user_id, transaction_type, amount, balance_after, description)
       VALUES (?, 'reset', 10, 10, 'Günlük token sıfırlaması')
   
4. Kullanıcı uygulamayı açtığında:
   └─> TokenProvider.refreshTokenInfo()
       └─> Güncellenmiş bakiye (10 token) görülür

Not: Bu cron job şu anda manuel olarak ayarlanmalıdır. Gelecekte Supabase Edge Functions ile otomatikleştirilebilir.


Akış 6: Token Geçmişi Görüntüleme#

1. Öğretmen profil sayfasında "Token Geçmişi" butonuna basar
   └─> Navigator.push(TokenHistoryScreen)
   
2. TokenHistoryScreen açılır:
   └─> initState()
       └─> WidgetsBinding.addPostFrameCallback
           └─> tokenProvider.loadTransactionHistory(refresh: true)
   
3. loadTransactionHistory():
   └─> RPC: get_token_history(limit: 20, offset: 0)
       └─> Result: {
             transactions: [...],
             total_count: 150,
             has_more: true
           }
   
4. İlk 20 işlem listelenir:
   └─> Tarih gruplandırması
   └─> Her işlem için:
       ├─> İkon ve renk (tipi göre)
       ├─> Açıklama
       ├─> Token miktarı
       └─> İşlem sonrası bakiye
   
5. Kullanıcı aşağı kaydırır:
   └─> _onScroll() tetiklenir
   └─> %80'e ulaşıldığında:
       └─> tokenProvider.loadTransactionHistory()
           └─> RPC: get_token_history(limit: 20, offset: 20)
   
6. Yeni 20 işlem listeye eklenir
   
7. has_more = false olana kadar devam eder

Akış 7: Supporter Plana Yükseltme (Gelecek)#

1. Öğretmen "Destekçi Ol" butonuna basar
   └─> UpgradeToSupporterDialog gösterilir
   
2. Dialog içeriği:
   ├─> Özellikler listesi
   └─> "Yakında" uyarısı
   
3. Gelecekte (Iyzico entegrasyonu sonrası):
   └─> Ödeme sayfasına yönlendirilir
       └─> Ödeme başarılı:
           ├─> UPDATE users
               SET account_type = 'supporter'
               WHERE id = ?
           ├─> INSERT token_transactions
               (transaction_type = 'add', description = 'Supporter plana yükseltme')
           └─> tokenProvider.refreshTokenInfo()
               └─> UI güncellenir: "Destekçi Planı" badge'i

Gelecek Geliştirmeler#

1. Ödeme Entegrasyonu (Iyzico)#

Hedef: Supporter plan için ödeme sistemi

Yapılacaklar:

  • Iyzico SDK entegrasyonu
  • Ödeme formu ekranı
  • Webhook endpoint (ödeme onayı için)
  • Plan upgrade logic
  • Fatura/makbuz sistemi

Dosyalar:

  • lib/common/services/payment_service.dart
  • lib/common/pages/payment_screen.dart
  • Backend webhook handler

2. Otomatik Token Sıfırlama#

Hedef: Günlük otomatik token sıfırlama

Yapılacaklar:

  • Supabase Edge Function oluşturma
  • Cron job yapılandırması (her gün 00:00)
  • RPC fonksiyon: reset_daily_tokens()
  • Notification gönderimi (opsiyonel)

Cron Job Yapılandırması:

# Supabase Dashboard > Database > Cron Jobs
# Her gün saat 00:00'da çalışacak
0 0 * * * http://your-project.supabase.co/functions/v1/reset-daily-tokens

3. Token Satın Alma (Ek Token Paketi)#

Hedef: Kullanıcılar ek token paketleri satın alabilir

5. Token Kullanım Analitikleri#

Hedef: Öğretmenlere token kullanım istatistikleri

Özellikler:

  • Haftalık/aylık kullanım grafikleri
  • En çok token harcanan günler
  • Ortalama günlük harcama
  • Toplam değerlendirilen kağıt sayısı

Yapılacaklar:

  • Analytics ekranı
  • Chart widget’ları (fl_chart paketi)

6. Bildirim Sistemi#

Hedef: Token durumu bildirimleri

Özellikler:

  • “Token bakiyeniz azaldı (3 kalan)” - %30 altı
  • “Tokenleriniz sıfırlandı (10 yeni token)” - günlük sıfırlama
  • “Supporter planına hoş geldiniz!” - upgrade

Yapılacaklar:

  • Push notification (Firebase Cloud Messaging)
  • In-app notification
  • Email bildirimleri (Supabase Email)

7. Referans Programı#

Hedef: Kullanıcı davet ederek token kazanma

Özellikler:

  • Benzersiz referans kodu
  • Her başarılı davet: +5 token (bonus)
  • Davet edilen: +2 token (hoş geldin bonusu)

8. Token Abonelik Modeli#

Hedef: Aylık/yıllık token abonelikleri

Notlar#

Güvenlik#

  • Tüm RPC fonksiyonlar SECURITY DEFINER ile çalışır
  • Row Level Security (RLS) politikaları aktif
  • Token işlemleri sadece authenticated kullanıcılar için
  • Kullanıcılar sadece kendi token bilgilerine erişebilir

Performans#

  • Token bilgileri cache’lenmez (her zaman güncel veri)
  • İşlem geçmişi sayfalama ile yüklenir (20 kayıt/sayfa)
  • İndeksler query performansını optimize eder