@@ -61,6 +61,14 @@ const resetAllUsageLimits = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const resetUserUsageLimits = (userId: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/usage-limits/reset/${userId}`,
|
||||
client: "admin",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
const getAllUsers = (params?: AdminPaginationParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedData<AdminUserDto>>>({
|
||||
@@ -119,6 +127,7 @@ export const adminService = {
|
||||
updateSetting,
|
||||
getAllUsageLimits,
|
||||
resetAllUsageLimits,
|
||||
resetUserUsageLimits,
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
deleteUser,
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface AdminUserDto {
|
||||
role?: string;
|
||||
isActive: boolean;
|
||||
subscription?: string;
|
||||
subscriptionStatus?: string;
|
||||
createdAt: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ export interface UpdateUserRoleDto {
|
||||
}
|
||||
|
||||
export interface UpdateUserSubscriptionDto {
|
||||
subscription: string;
|
||||
plan: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
|
||||
@@ -66,6 +66,18 @@ export const useResetAllUsageLimits = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetUserUsageLimits = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (userId: string) => adminService.resetUserUsageLimits(userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usageLimits() });
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
export const useAdminUsers = (
|
||||
params?: AdminPaginationParams,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import type { AnalyzeMatchesDto, AnalysisResultDto, AnalysisHistoryDto } from "./types";
|
||||
import type {
|
||||
AnalyzeMatchesDto,
|
||||
AnalysisResultDto,
|
||||
AnalysisHistoryDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Analysis Service
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clientMap } from '@/lib/api/client-map';
|
||||
import { Method } from 'axios';
|
||||
import { clientMap } from "@/lib/api/client-map";
|
||||
import { Method } from "axios";
|
||||
|
||||
interface ApiRequestOptions {
|
||||
url: string;
|
||||
@@ -9,14 +9,21 @@ interface ApiRequestOptions {
|
||||
params?: Record<string, string | number | boolean | undefined> | object;
|
||||
}
|
||||
|
||||
export async function apiRequest<T = unknown>(options: ApiRequestOptions): Promise<T> {
|
||||
const { url, client, method = 'get', data, params } = options;
|
||||
export async function apiRequest<T = unknown>(
|
||||
options: ApiRequestOptions,
|
||||
): Promise<T> {
|
||||
const { url, client, method = "get", data, params } = options;
|
||||
const clientInstance = clientMap[client];
|
||||
|
||||
if (!url || !clientInstance) {
|
||||
throw new Error(`Invalid API request: ${client} - ${url}`);
|
||||
}
|
||||
|
||||
const response = await clientInstance.request<T>({ method, url, data, params });
|
||||
const response = await clientInstance.request<T>({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
LoginDto,
|
||||
AuthResponse,
|
||||
RegisterDto,
|
||||
RefreshTokenDto,
|
||||
} from "./types";
|
||||
import { LoginDto, AuthResponse, RegisterDto, RefreshTokenDto } from "./types";
|
||||
|
||||
const login = (data: LoginDto) => {
|
||||
return apiRequest<ApiResponse<AuthResponse>>({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import baseUrl from '@/config/base-url';
|
||||
import { createApiClient } from '@/lib/api/create-api-client';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import baseUrl from "@/config/base-url";
|
||||
import { createApiClient } from "@/lib/api/create-api-client";
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
export const clientMap: Record<keyof typeof baseUrl, AxiosInstance> = {
|
||||
auth: createApiClient(baseUrl.auth!),
|
||||
|
||||
@@ -90,4 +90,3 @@ export const couponsService = {
|
||||
suggestCoupon,
|
||||
generateFrequencyCoupon,
|
||||
};
|
||||
|
||||
|
||||
@@ -63,4 +63,3 @@ export const useGenerateFrequencyCoupon = () => {
|
||||
couponsService.generateFrequencyCoupon(dto),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -171,10 +171,10 @@ export interface OddsBandEntryDto {
|
||||
odd_rate?: number;
|
||||
even_rate?: number;
|
||||
"1x_rate"?: number;
|
||||
"x2_rate"?: number;
|
||||
x2_rate?: number;
|
||||
"12_rate"?: number;
|
||||
"1x_sample"?: number;
|
||||
"x2_sample"?: number;
|
||||
x2_sample?: number;
|
||||
"12_sample"?: number;
|
||||
sample: number;
|
||||
[key: string]: unknown;
|
||||
@@ -208,9 +208,15 @@ export interface TripleValueEntryDto {
|
||||
}
|
||||
|
||||
export type HtftComboKey =
|
||||
| "11" | "1x" | "12"
|
||||
| "x1" | "xx" | "x2"
|
||||
| "21" | "2x" | "22";
|
||||
| "11"
|
||||
| "1x"
|
||||
| "12"
|
||||
| "x1"
|
||||
| "xx"
|
||||
| "x2"
|
||||
| "21"
|
||||
| "2x"
|
||||
| "22";
|
||||
|
||||
export interface V27EngineDto {
|
||||
version: string;
|
||||
|
||||
@@ -17,7 +17,7 @@ export const usePrediction = (matchId: string) => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.detail(matchId),
|
||||
queryFn: () => predictionsService.getPrediction(matchId),
|
||||
enabled: !!matchId,
|
||||
enabled: false, // DO NOT auto-fetch since it consumes limits
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,13 +55,15 @@ export const useSyncBulletin = () => {
|
||||
|
||||
export const useGenerateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: GenerateColumnsDto) => sporTotoService.generateColumns(dto),
|
||||
mutationFn: (dto: GenerateColumnsDto) =>
|
||||
sporTotoService.generateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useEvaluateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: EvaluateColumnsDto) => sporTotoService.evaluateColumns(dto),
|
||||
mutationFn: (dto: EvaluateColumnsDto) =>
|
||||
sporTotoService.evaluateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export { subscriptionService } from "./service";
|
||||
export {
|
||||
useSubscriptionPlans,
|
||||
useMySubscription,
|
||||
useCheckoutConfig,
|
||||
useCancelSubscription,
|
||||
SubscriptionQueryKeys,
|
||||
} from "./use-hooks";
|
||||
export type {
|
||||
PlanType,
|
||||
BillingInterval,
|
||||
PlanInfo,
|
||||
SubscriptionDto,
|
||||
UsageLimitDto,
|
||||
CheckoutConfigDto,
|
||||
CreateCheckoutRequest,
|
||||
CancelSubscriptionRequest,
|
||||
} from "./types";
|
||||
export {
|
||||
PlanType as PlanTypeEnum,
|
||||
BillingInterval as BillingIntervalEnum,
|
||||
} from "./types";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import type { ApiResponse } from "@/types/api-response";
|
||||
import type {
|
||||
PlanInfo,
|
||||
SubscriptionDto,
|
||||
CheckoutConfigDto,
|
||||
CreateCheckoutRequest,
|
||||
CancelSubscriptionRequest,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Subscriptions Service
|
||||
* Backend: /api/subscriptions/*
|
||||
*/
|
||||
|
||||
const getPlans = (): Promise<ApiResponse<readonly PlanInfo[]>> => {
|
||||
return apiRequest<ApiResponse<readonly PlanInfo[]>>({
|
||||
url: "/subscriptions/plans",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getMySubscription = (): Promise<ApiResponse<SubscriptionDto | null>> => {
|
||||
return apiRequest<ApiResponse<SubscriptionDto | null>>({
|
||||
url: "/subscriptions/me",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getCheckoutConfig = (
|
||||
dto: CreateCheckoutRequest,
|
||||
): Promise<ApiResponse<CheckoutConfigDto>> => {
|
||||
return apiRequest<ApiResponse<CheckoutConfigDto>>({
|
||||
url: "/subscriptions/checkout",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const cancelSubscription = (
|
||||
dto: CancelSubscriptionRequest,
|
||||
): Promise<ApiResponse<null>> => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: "/subscriptions/cancel",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
export const subscriptionService = {
|
||||
getPlans,
|
||||
getMySubscription,
|
||||
getCheckoutConfig,
|
||||
cancelSubscription,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// ========================
|
||||
// Enums — mirrors backend PlanType / BillingIntervalType
|
||||
// ========================
|
||||
|
||||
export enum PlanType {
|
||||
FREE = "free",
|
||||
PLUS = "plus",
|
||||
PREMIUM = "premium",
|
||||
}
|
||||
|
||||
export enum BillingInterval {
|
||||
MONTHLY = "monthly",
|
||||
YEARLY = "yearly",
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface UsageLimitDto {
|
||||
analysisCount: number;
|
||||
couponCount: number;
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
}
|
||||
|
||||
export interface SubscriptionDto {
|
||||
id: string;
|
||||
plan: PlanType;
|
||||
billingInterval: BillingInterval | null;
|
||||
currentPeriodStart: string | null;
|
||||
currentPeriodEnd: string | null;
|
||||
cancelledAt: string | null;
|
||||
cancelEffectiveDate: string | null;
|
||||
paddlePriceId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PlanLimits {
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
}
|
||||
|
||||
export interface PlanInfo {
|
||||
id: PlanType;
|
||||
name: string;
|
||||
description: string;
|
||||
monthlyPrice: number;
|
||||
yearlyPrice: number;
|
||||
currency: string;
|
||||
features: string[];
|
||||
limits: PlanLimits;
|
||||
highlighted: boolean;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface CreateCheckoutRequest {
|
||||
plan: PlanType;
|
||||
billingInterval: BillingInterval;
|
||||
}
|
||||
|
||||
export interface CancelSubscriptionRequest {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Checkout Config Response
|
||||
// ========================
|
||||
|
||||
export interface CheckoutConfigDto {
|
||||
priceId: string;
|
||||
clientToken: string;
|
||||
environment: string;
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { subscriptionService } from "./service";
|
||||
import type { CreateCheckoutRequest, CancelSubscriptionRequest } from "./types";
|
||||
|
||||
export const SubscriptionQueryKeys = {
|
||||
all: ["subscriptions"] as const,
|
||||
plans: () => [...SubscriptionQueryKeys.all, "plans"] as const,
|
||||
me: () => [...SubscriptionQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all available plans (public, no auth required)
|
||||
*/
|
||||
export const useSubscriptionPlans = () => {
|
||||
return useQuery({
|
||||
queryKey: SubscriptionQueryKeys.plans(),
|
||||
queryFn: () => subscriptionService.getPlans(),
|
||||
staleTime: 1000 * 60 * 30, // Plans rarely change — 30 min cache
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch current user subscription status
|
||||
*/
|
||||
export const useMySubscription = (enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
queryFn: () => subscriptionService.getMySubscription(),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Paddle checkout config for a plan
|
||||
*/
|
||||
export const useCheckoutConfig = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: CreateCheckoutRequest) =>
|
||||
subscriptionService.getCheckoutConfig(dto),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the current subscription
|
||||
*/
|
||||
export const useCancelSubscription = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (dto: CancelSubscriptionRequest) =>
|
||||
subscriptionService.cancelSubscription(dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,14 @@ export interface ChangePasswordDto {
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface UsageLimitDto {
|
||||
analysisCount: number;
|
||||
couponCount: number;
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
lastResetDate: string;
|
||||
}
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -25,6 +33,7 @@ export interface UserResponseDto {
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
usageLimit?: UsageLimitDto;
|
||||
}
|
||||
|
||||
const getMe = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { usersService } from "./service";
|
||||
import type { UpdateProfileDto, ChangePasswordDto } from "./service";
|
||||
|
||||
@@ -7,6 +7,14 @@ export const UsersQueryKeys = {
|
||||
me: () => [...UsersQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
export const useGetMe = (enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: UsersQueryKeys.me(),
|
||||
queryFn: () => usersService.getMe(),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProfile = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: UpdateProfileDto) => usersService.updateMe(dto),
|
||||
|
||||
@@ -9,7 +9,9 @@ function randomToken() {
|
||||
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
}
|
||||
|
||||
const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
|
||||
const isMockMode =
|
||||
process.env.NODE_ENV !== "production" &&
|
||||
process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
@@ -21,51 +23,42 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
async authorize(credentials) {
|
||||
try {
|
||||
console.log("Starting authorization with:", {
|
||||
email: credentials?.email,
|
||||
});
|
||||
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error("Email ve şifre gereklidir.");
|
||||
}
|
||||
|
||||
// Eğer mock mod aktifse backend'e gitme
|
||||
if (isMockMode) {
|
||||
console.log("Mock mode active, bypassing backend");
|
||||
return {
|
||||
id: credentials.email,
|
||||
name: credentials.email.split("@")[0],
|
||||
email: credentials.email,
|
||||
accessToken: randomToken(),
|
||||
refreshToken: randomToken(),
|
||||
subscriptionPlan: "free",
|
||||
};
|
||||
}
|
||||
|
||||
// Normal mod: backend'e istek at
|
||||
console.log("Sending login request to backend...");
|
||||
const res = await authService.login({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Backend response received:",
|
||||
JSON.stringify(res, null, 2),
|
||||
);
|
||||
|
||||
const response = res;
|
||||
|
||||
// Backend returns ApiResponse<TokenResponseDto>
|
||||
// Structure: { data: { accessToken, refreshToken, expiresIn, user }, message, statusCode }
|
||||
if (!res.success || !response?.data?.accessToken) {
|
||||
console.error("Login failed or no access token in response");
|
||||
throw new Error(response?.message || "Giriş başarısız");
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, user } = response.data;
|
||||
const normalizedRoles = normalizeRoles(user.roles);
|
||||
|
||||
console.log("Login successful, creating user session object");
|
||||
// Extract subscription plan from user data (backend includes subscriptionStatus)
|
||||
const subscriptionPlan = (user as Record<string, unknown>)
|
||||
.subscriptionPlan as "free" | "plus" | "premium" | undefined;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
@@ -76,15 +69,14 @@ export const authOptions: NextAuthOptions = {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
roles: normalizedRoles,
|
||||
subscriptionPlan: subscriptionPlan ?? "free",
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
console.error("Authorize error detailed:", error);
|
||||
const err = error as Error & {
|
||||
response?: { data: unknown; status: number };
|
||||
};
|
||||
if (err.response) {
|
||||
console.error("Error response data:", err.response.data);
|
||||
console.error("Error response status:", err.response.status);
|
||||
// keep error logging for actual errors but remove verbose debugging
|
||||
}
|
||||
throw new Error(
|
||||
err.message || "An error occurred during authentication",
|
||||
@@ -100,12 +92,14 @@ export const authOptions: NextAuthOptions = {
|
||||
token.refreshToken = user.refreshToken;
|
||||
token.id = user.id;
|
||||
token.roles = normalizeRoles(user.roles);
|
||||
token.subscriptionPlan = user.subscriptionPlan ?? "free";
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }: { session: Session; token: JWT }) {
|
||||
session.user.id = token.id;
|
||||
session.user.roles = normalizeRoles(token.roles);
|
||||
session.user.subscriptionPlan = token.subscriptionPlan ?? "free";
|
||||
session.accessToken = token.accessToken;
|
||||
session.refreshToken = token.refreshToken;
|
||||
return session;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export { PaddleProvider, usePaddle, CheckoutEventNames } from "./paddle-provider";
|
||||
export type { PaddleEventData } from "./paddle-provider";
|
||||
export { usePaddleCheckout } from "./use-paddle-checkout";
|
||||
export { paddleConfig } from "./paddle-config";
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Paddle client-side configuration.
|
||||
*
|
||||
* These values are read from NEXT_PUBLIC_* env vars so they are
|
||||
* available in the browser bundle.
|
||||
*/
|
||||
|
||||
export const paddleConfig = {
|
||||
/** Paddle client-side token (public — safe for browser) */
|
||||
clientToken: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN ?? "",
|
||||
|
||||
/** 'sandbox' | 'production' */
|
||||
environment: (process.env.NEXT_PUBLIC_PADDLE_ENVIRONMENT ?? "sandbox") as
|
||||
| "sandbox"
|
||||
| "production",
|
||||
|
||||
/** Paddle seller ID — used for Paddle.js initialization */
|
||||
sellerId: process.env.NEXT_PUBLIC_PADDLE_SELLER_ID ?? "",
|
||||
} as const;
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type {
|
||||
Paddle,
|
||||
CheckoutOpenOptions,
|
||||
} from "@paddle/paddle-js";
|
||||
import { initializePaddle } from "@paddle/paddle-js";
|
||||
import type { PaddleEventData } from "@paddle/paddle-js/types/checkout/checkout";
|
||||
import { CheckoutEventNames } from "@paddle/paddle-js/types/checkout/events";
|
||||
import { paddleConfig } from "./paddle-config";
|
||||
|
||||
type CheckoutEventHandler = (event: PaddleEventData) => void;
|
||||
|
||||
interface PaddleContextValue {
|
||||
paddle: Paddle | null;
|
||||
isReady: boolean;
|
||||
openCheckout: (options: CheckoutOpenOptions) => void;
|
||||
onCheckoutEvent: (handler: CheckoutEventHandler) => () => void;
|
||||
}
|
||||
|
||||
const PaddleContext = createContext<PaddleContextValue>({
|
||||
paddle: null,
|
||||
isReady: false,
|
||||
openCheckout: () => {},
|
||||
onCheckoutEvent: () => () => {},
|
||||
});
|
||||
|
||||
export function PaddleProvider({ children }: { children: ReactNode }) {
|
||||
const [paddle, setPaddle] = useState<Paddle | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const listenersRef = useRef<Set<CheckoutEventHandler>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!paddleConfig.clientToken) {
|
||||
console.warn("[Paddle] Missing NEXT_PUBLIC_PADDLE_CLIENT_TOKEN");
|
||||
return;
|
||||
}
|
||||
|
||||
initializePaddle({
|
||||
token: paddleConfig.clientToken,
|
||||
environment: paddleConfig.environment,
|
||||
eventCallback: (event: PaddleEventData) => {
|
||||
listenersRef.current.forEach((handler) => handler(event));
|
||||
},
|
||||
})
|
||||
.then((paddleInstance) => {
|
||||
if (paddleInstance) {
|
||||
setPaddle(paddleInstance);
|
||||
setIsReady(true);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("[Paddle] Initialization failed:", err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openCheckout = useCallback(
|
||||
(options: CheckoutOpenOptions) => {
|
||||
if (!paddle) {
|
||||
console.error("[Paddle] Not initialized — cannot open checkout");
|
||||
return;
|
||||
}
|
||||
paddle.Checkout.open(options);
|
||||
},
|
||||
[paddle],
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscribe to Paddle checkout events.
|
||||
* Returns an unsubscribe function for cleanup.
|
||||
*/
|
||||
const onCheckoutEvent = useCallback(
|
||||
(handler: CheckoutEventHandler): (() => void) => {
|
||||
listenersRef.current.add(handler);
|
||||
return () => {
|
||||
listenersRef.current.delete(handler);
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PaddleContext.Provider
|
||||
value={{ paddle, isReady, openCheckout, onCheckoutEvent }}
|
||||
>
|
||||
{children}
|
||||
</PaddleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the Paddle instance, checkout helper, and event subscription.
|
||||
*/
|
||||
export function usePaddle(): PaddleContextValue {
|
||||
return useContext(PaddleContext);
|
||||
}
|
||||
|
||||
export { CheckoutEventNames };
|
||||
export type { PaddleEventData };
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePaddle, CheckoutEventNames } from "./paddle-provider";
|
||||
import type { PaddleEventData } from "./paddle-provider";
|
||||
import { useCheckoutConfig } from "@/lib/api/subscriptions/use-hooks";
|
||||
import type { PlanType, BillingInterval } from "@/lib/api/subscriptions/types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { SubscriptionQueryKeys } from "@/lib/api/subscriptions/use-hooks";
|
||||
|
||||
interface UsePaddleCheckoutReturn {
|
||||
startCheckout: (plan: PlanType, interval: BillingInterval) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that orchestrates the Paddle checkout flow:
|
||||
* 1. Calls backend to get priceId + clientToken
|
||||
* 2. Opens Paddle overlay checkout
|
||||
* 3. Listens for Paddle checkout.completed event to invalidate subscription cache
|
||||
*/
|
||||
export function usePaddleCheckout(): UsePaddleCheckoutReturn {
|
||||
const { openCheckout, onCheckoutEvent } = usePaddle();
|
||||
const { data: session } = useSession();
|
||||
const checkoutConfig = useCheckoutConfig();
|
||||
const queryClient = useQueryClient();
|
||||
const isSubscribedRef = useRef(false);
|
||||
|
||||
// Subscribe to Paddle events once on mount
|
||||
useEffect(() => {
|
||||
if (isSubscribedRef.current) return;
|
||||
isSubscribedRef.current = true;
|
||||
|
||||
const unsubscribe = onCheckoutEvent((event: PaddleEventData) => {
|
||||
if (event.name === CheckoutEventNames.CHECKOUT_COMPLETED) {
|
||||
// Paddle confirmed the payment — refresh subscription state
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
});
|
||||
}
|
||||
|
||||
if (event.name === CheckoutEventNames.CHECKOUT_CLOSED) {
|
||||
// User closed checkout — refresh in case a payment was already processed
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isSubscribedRef.current = false;
|
||||
unsubscribe();
|
||||
};
|
||||
}, [onCheckoutEvent, queryClient]);
|
||||
|
||||
const startCheckout = useCallback(
|
||||
async (plan: PlanType, interval: BillingInterval) => {
|
||||
if (!session?.user?.email) {
|
||||
console.error("[Paddle] User not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await checkoutConfig.mutateAsync({
|
||||
plan,
|
||||
billingInterval: interval,
|
||||
});
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
console.error("[Paddle] Failed to get checkout config");
|
||||
return;
|
||||
}
|
||||
|
||||
const { priceId } = result.data;
|
||||
|
||||
openCheckout({
|
||||
items: [{ priceId, quantity: 1 }],
|
||||
customer: {
|
||||
email: session.user.email,
|
||||
},
|
||||
customData: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
settings: {
|
||||
displayMode: "overlay",
|
||||
theme: "dark",
|
||||
locale: "tr",
|
||||
successUrl: `${window.location.origin}/profile?checkout=success`,
|
||||
},
|
||||
});
|
||||
},
|
||||
[openCheckout, session, checkoutConfig],
|
||||
);
|
||||
|
||||
return {
|
||||
startCheckout,
|
||||
isLoading: checkoutConfig.isPending,
|
||||
};
|
||||
}
|
||||
@@ -76,7 +76,7 @@ type PromptContent =
|
||||
export async function generateAIContent(
|
||||
model: string,
|
||||
prompt: PromptContent,
|
||||
config?: GenerationConfig
|
||||
config?: GenerationConfig,
|
||||
) {
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
@@ -100,7 +100,7 @@ export async function generateAIContent(
|
||||
*/
|
||||
export async function generateText(
|
||||
model: string,
|
||||
prompt: string
|
||||
prompt: string,
|
||||
): Promise<{ text: string; usage?: UsageMetadata }> {
|
||||
const response = await generateAIContent(model, prompt);
|
||||
return {
|
||||
@@ -119,7 +119,7 @@ export async function generateText(
|
||||
export async function generateWithImage(
|
||||
model: string,
|
||||
imageBase64: string,
|
||||
textPrompt: string
|
||||
textPrompt: string,
|
||||
) {
|
||||
const response = await generateAIContent(model, [
|
||||
{
|
||||
@@ -151,7 +151,7 @@ export async function generateImage(
|
||||
imageBase64?: string,
|
||||
suggestions?: string[],
|
||||
model: string = AI_MODELS.FLASH_IMAGE,
|
||||
aspectRatio?: string
|
||||
aspectRatio?: string,
|
||||
): Promise<{ imageUrl: string | null; usage?: UsageMetadata }> {
|
||||
let parts: Array<
|
||||
{ text: string } | { inlineData: { mimeType: string; data: string } }
|
||||
@@ -166,7 +166,7 @@ export async function generateImage(
|
||||
// Görüntü düzenleme modu
|
||||
const cleanBase64 = imageBase64.replace(
|
||||
/^data:image\/(png|jpeg|webp|jpg);base64,/,
|
||||
""
|
||||
"",
|
||||
);
|
||||
|
||||
// Eğer Nano Banana modeli ve öneriler varsa, prompt'u zenginleştir
|
||||
@@ -200,8 +200,8 @@ export async function generateImage(
|
||||
});
|
||||
|
||||
const candidate = response.candidates?.[0];
|
||||
const imagePart = candidate?.content?.parts?.find(
|
||||
(p) => p.inlineData?.mimeType?.startsWith("image/")
|
||||
const imagePart = candidate?.content?.parts?.find((p) =>
|
||||
p.inlineData?.mimeType?.startsWith("image/"),
|
||||
);
|
||||
|
||||
if (imagePart?.inlineData?.data) {
|
||||
@@ -212,7 +212,10 @@ export async function generateImage(
|
||||
}
|
||||
|
||||
console.warn("No image part found in response");
|
||||
return { imageUrl: null, usage: response.usageMetadata as UsageMetadata | undefined };
|
||||
return {
|
||||
imageUrl: null,
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("generateImage error:", error);
|
||||
return { imageUrl: null };
|
||||
@@ -226,7 +229,9 @@ export async function generateImage(
|
||||
* @param usage - Yapay zeka yanıtından kullanım meta verileri
|
||||
* @returns Para birimi cinsinden toplam maliyet
|
||||
*/
|
||||
export async function calculateAICost(usage: UsageMetadata | null | undefined): Promise<number> {
|
||||
export async function calculateAICost(
|
||||
usage: UsageMetadata | null | undefined,
|
||||
): Promise<number> {
|
||||
if (!usage) return 0;
|
||||
|
||||
const pricing = await getPricing();
|
||||
|
||||
+12
-12
@@ -4,18 +4,18 @@
|
||||
*/
|
||||
|
||||
export const SUPPORTED_LOCALES = {
|
||||
tr: 'Turkish',
|
||||
en: 'English',
|
||||
de: 'German',
|
||||
ar: 'Arabic',
|
||||
zh: 'Chinese',
|
||||
es: 'Spanish',
|
||||
fr: 'French',
|
||||
it: 'Italian',
|
||||
ja: 'Japanese',
|
||||
ko: 'Korean',
|
||||
pt: 'Portuguese',
|
||||
ru: 'Russian',
|
||||
tr: "Turkish",
|
||||
en: "English",
|
||||
de: "German",
|
||||
ar: "Arabic",
|
||||
zh: "Chinese",
|
||||
es: "Spanish",
|
||||
fr: "French",
|
||||
it: "Italian",
|
||||
ja: "Japanese",
|
||||
ko: "Korean",
|
||||
pt: "Portuguese",
|
||||
ru: "Russian",
|
||||
} as const;
|
||||
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES;
|
||||
|
||||
@@ -7,28 +7,36 @@ export const buildPhotorealisticPrompt = (corePrompt: string): string => {
|
||||
const blueprint = {
|
||||
subject_context: corePrompt,
|
||||
style: {
|
||||
direction: 'Professional Lifestyle Photography / Authentic Brand Content',
|
||||
aesthetic: 'Modern, bright, engaging, premium but approachable',
|
||||
lighting: 'Natural ambient daylight mixed with soft studio fill, warm tones',
|
||||
direction: "Professional Lifestyle Photography / Authentic Brand Content",
|
||||
aesthetic: "Modern, bright, engaging, premium but approachable",
|
||||
lighting:
|
||||
"Natural ambient daylight mixed with soft studio fill, warm tones",
|
||||
},
|
||||
camera_settings: {
|
||||
sensor: 'Full-frame Digital Sensor (Sony Alpha / Canon R5)',
|
||||
lens: '35mm or 50mm Prime (Standard Social Media View)',
|
||||
depth_of_field: 'Moderate depth of field (sharp subject, slightly softened background)',
|
||||
shutter: '1/125s natural motion freeze',
|
||||
sensor: "Full-frame Digital Sensor (Sony Alpha / Canon R5)",
|
||||
lens: "35mm or 50mm Prime (Standard Social Media View)",
|
||||
depth_of_field:
|
||||
"Moderate depth of field (sharp subject, slightly softened background)",
|
||||
shutter: "1/125s natural motion freeze",
|
||||
},
|
||||
composition_rules: {
|
||||
framing: 'Rule of thirds, center-weighted for social media engagement',
|
||||
angle: 'Eye-level or 45-degree isometric (depending on subject)',
|
||||
aspect_ratio_fit: 'Optimized for Instagram/LinkedIn',
|
||||
framing: "Rule of thirds, center-weighted for social media engagement",
|
||||
angle: "Eye-level or 45-degree isometric (depending on subject)",
|
||||
aspect_ratio_fit: "Optimized for Instagram/LinkedIn",
|
||||
},
|
||||
quality_assurance: {
|
||||
clarity: 'Perfect Focus',
|
||||
noise_level: 'Minimal natural grain allowed for authenticity',
|
||||
texture_detail: 'High fidelity materials',
|
||||
render_engine: 'Photorealistic Photography Style (Not CGI looking)',
|
||||
clarity: "Perfect Focus",
|
||||
noise_level: "Minimal natural grain allowed for authenticity",
|
||||
texture_detail: "High fidelity materials",
|
||||
render_engine: "Photorealistic Photography Style (Not CGI looking)",
|
||||
},
|
||||
prohibited_elements: ['text watermarks', 'blurry', 'distorted', 'ugly', 'low resolution'],
|
||||
prohibited_elements: [
|
||||
"text watermarks",
|
||||
"blurry",
|
||||
"distorted",
|
||||
"ugly",
|
||||
"low resolution",
|
||||
],
|
||||
};
|
||||
|
||||
return JSON.stringify(blueprint, null, 2);
|
||||
|
||||
Reference in New Issue
Block a user