feat: ui polish, docs, api hardening, and common pages

This commit is contained in:
Dwindi Ramadhana
2026-02-06 14:04:41 +07:00
parent 0f602c12bc
commit 844ad4901b
18 changed files with 1106 additions and 128 deletions

View File

@@ -398,6 +398,23 @@ class EmojiApiController extends Controller
private function trackDailyUsage(Request $request, string $q, string $category, string $subcategory): array
{
$dailyLimit = max((int) config('dewemoji.free_daily_limit', 30), 1);
$rateLimitEnabled = (bool) config('dewemoji.rate_limit_enabled', true);
// Local development should not silently look broken because of daily metering.
if (!$rateLimitEnabled || app()->environment('local')) {
return [
'blocked' => false,
'meta' => [
'used' => 0,
'limit' => $dailyLimit,
'remaining' => $dailyLimit,
'window' => 'daily',
'window_ends_at' => Carbon::tomorrow('UTC')->toIso8601String(),
'count_basis' => 'distinct_query',
'metering' => 'disabled',
],
];
}
$key = trim((string) $request->query('key', ''));
if ($key === '') {

View File

@@ -131,10 +131,14 @@ class SiteController extends Controller
$items = $decoded['emojis'] ?? [];
$match = null;
$byEmoji = [];
foreach ($items as $item) {
$char = (string) ($item['emoji'] ?? '');
if ($char !== '' && !isset($byEmoji[$char])) {
$byEmoji[$char] = $item;
}
if (($item['slug'] ?? '') === $slug) {
$match = $item;
break;
}
}
@@ -142,8 +146,20 @@ class SiteController extends Controller
abort(404);
}
$relatedDetails = [];
foreach (array_slice($match['related'] ?? [], 0, 8) as $relatedEmoji) {
$relatedEmoji = (string) $relatedEmoji;
$ref = $byEmoji[$relatedEmoji] ?? null;
$relatedDetails[] = [
'emoji' => $relatedEmoji,
'slug' => (string) ($ref['slug'] ?? ''),
'name' => (string) ($ref['name'] ?? $relatedEmoji),
];
}
return view('site.emoji-detail', [
'emoji' => $match,
'relatedDetails' => $relatedDetails,
'canonicalPath' => '/emoji/'.$slug,
]);
}

View File

@@ -175,6 +175,15 @@ class LicenseVerificationService
}
$purchase = is_array($json['purchase'] ?? null) ? $json['purchase'] : [];
if (!$this->isTruthy($purchase['is_valid'] ?? true)) {
continue;
}
if ($this->isTruthy($purchase['refunded'] ?? false)) {
continue;
}
if ($this->isTruthy($purchase['chargebacked'] ?? false)) {
continue;
}
$isRecurring = !empty($purchase['recurrence']);
return [
@@ -231,6 +240,10 @@ class LicenseVerificationService
if ($apiKey === '') {
$apiKey = trim((string) config('dewemoji.billing.providers.mayar.secret_key', ''));
}
$productIds = config('dewemoji.billing.providers.mayar.product_ids', []);
if (!is_array($productIds)) {
$productIds = [];
}
if ($url === '') {
return ['ok' => false, 'err' => 'mayar_missing_url'];
}
@@ -240,10 +253,15 @@ class LicenseVerificationService
$request = Http::timeout($timeout)
->withHeaders(['Accept' => 'application/json']);
if ($apiKey !== '') {
$request = $request->withToken($apiKey);
$request = $request->withToken($apiKey)
->withHeaders(['X-API-Key' => $apiKey]);
}
$response = $request->post($url, ['license_key' => $key]);
$response = $request->post($url, [
'license_key' => $key,
'license' => $key,
'key' => $key,
]);
if (!$response->successful()) {
return ['ok' => false, 'err' => 'mayar_http_'.$response->status()];
@@ -255,17 +273,38 @@ class LicenseVerificationService
}
$data = is_array($json['data'] ?? null) ? $json['data'] : [];
$valid = (($json['success'] ?? false) === true) || (($json['valid'] ?? false) === true) || (($data['valid'] ?? false) === true);
$status = strtolower((string) ($data['status'] ?? $json['status'] ?? ''));
$valid = (($json['success'] ?? false) === true)
|| (($json['valid'] ?? false) === true)
|| (($data['valid'] ?? false) === true)
|| in_array($status, ['active', 'paid', 'completed', 'valid'], true);
if (!$valid) {
return ['ok' => false, 'err' => 'mayar_invalid'];
}
$productId = $this->firstString([
$data['product_id'] ?? null,
$data['productId'] ?? null,
$data['product_code'] ?? null,
$json['product_id'] ?? null,
]);
if (!empty($productIds) && ($productId === null || !in_array($productId, $productIds, true))) {
return ['ok' => false, 'err' => 'mayar_no_match'];
}
$planType = strtolower((string) ($data['type'] ?? 'lifetime'));
$expiresAt = $this->firstString([
$data['expires_at'] ?? null,
$data['expired_at'] ?? null,
$data['expiry_date'] ?? null,
$data['valid_until'] ?? null,
$json['expires_at'] ?? null,
]);
return [
'ok' => true,
'plan' => 'pro',
'product_id' => (string) ($data['product_id'] ?? '') ?: null,
'expires_at' => isset($data['expires_at']) ? (string) $data['expires_at'] : null,
'product_id' => $productId,
'expires_at' => $expiresAt,
'meta' => [
'plan_type' => $planType,
],
@@ -332,4 +371,38 @@ class LicenseVerificationService
'details' => $details,
];
}
private function isTruthy(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_numeric($value)) {
return (int) $value === 1;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'true', 'yes', 'y', 'on'], true);
}
return false;
}
/**
* @param array<int,mixed> $values
*/
private function firstString(array $values): ?string
{
foreach ($values as $value) {
if (!is_string($value)) {
continue;
}
$value = trim($value);
if ($value !== '') {
return $value;
}
}
return null;
}
}