diff --git a/app.json b/app.json
index 3d5095e..ef3c305 100644
--- a/app.json
+++ b/app.json
@@ -38,6 +38,19 @@
"backgroundColor": "#000000"
}
}
+ ],
+ [
+ "expo-font",
+ {
+ "fonts": [
+ "./assets/fonts/PlusJakartaSans-Bold.ttf",
+ "./assets/fonts/PlusJakartaSans-ExtraBold.ttf",
+ "./assets/fonts/PlusJakartaSans-Light.ttf",
+ "./assets/fonts/PlusJakartaSans-Medium.ttf",
+ "./assets/fonts/PlusJakartaSans-Regular.ttf",
+ "./assets/fonts/PlusJakartaSans-SemiBold.ttf"
+ ]
+ }
]
],
"experiments": {
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 67e9bda..7fdba70 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -1,37 +1,99 @@
+import ListHeading from "@/components/ListHeading";
+import SubscriptionCard from "@/components/SubscriptionCard";
+import UpcomingSubscriptionCard from "@/components/UpcomingSubscriptionCard";
+import {
+ HOME_BALANCE,
+ HOME_SUBSCRIPTIONS,
+ HOME_USER,
+ UPCOMING_SUBSCRIPTIONS,
+} from "@/constants/data";
+import icons from "@/constants/icons";
+import images from "@/constants/images";
import "@/global.css";
-import { Link } from "expo-router";
+import { formatCurrency } from "@/lib/utils/currency";
+import dayjs from "dayjs";
+import { useRouter } from "expo-router";
import { styled } from "nativewind";
-import { Text } from "react-native";
+import { useState } from "react";
+import { FlatList, Image, Text, View } from "react-native";
import { SafeAreaView as RNSafeAreaView } from "react-native-safe-area-context";
const SafeAreaView = styled(RNSafeAreaView);
export default function App() {
+ const router = useRouter();
+ const [expandedSubscriptionId, setExpandedSubscriptionId] = useState<
+ string | null
+ >(null);
+
return (
-
- Welcome to Nativewind!
-
-
- Sign In
-
-
- Sign Up
-
+ (
+ <>
+
+
+
+ {HOME_USER.name}
+
+
+
+
+
+ Balance
+
+
+
+ {formatCurrency(HOME_BALANCE.amount)}
+
+
+ {dayjs(HOME_BALANCE.nextRenewalDate).format("MM/DD")}
+
+
+
+
+
+ router.push("/(tabs)/subscriptions")} />
+ (
+
+ )}
+ keyExtractor={(item) => item.id}
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ ListEmptyComponent={
+
+ No upcoming subscriptions yet.
+
+ }
+ />
+
- {/* Subscription Examples */}
-
- Claude Max Subscription
-
+ router.push("/(tabs)/subscriptions")} />
+ >
+ )}
+ data={HOME_SUBSCRIPTIONS}
+ keyExtractor={(item) => item.id}
+ renderItem={({ item }) => (
+ {
+ setExpandedSubscriptionId((currentId) =>
+ currentId === item.id ? null : item.id,
+ );
+ }}
+ />
+ )}
+ extraData={expandedSubscriptionId}
+ ItemSeparatorComponent={() => }
+ ListEmptyComponent={
+ No subscriptions yet.
+ }
+ contentContainerClassName="pb-28"
+ />
);
}
diff --git a/app/subscriptions/[id].tsx b/app/(tabs)/subscriptions/[id].tsx
similarity index 59%
rename from app/subscriptions/[id].tsx
rename to app/(tabs)/subscriptions/[id].tsx
index 9db5de1..480d192 100644
--- a/app/subscriptions/[id].tsx
+++ b/app/(tabs)/subscriptions/[id].tsx
@@ -1,17 +1,21 @@
import { Link, useLocalSearchParams } from "expo-router";
+import { styled } from "nativewind";
import React from "react";
-import { Text, View } from "react-native";
+import { Text } from "react-native";
+import { SafeAreaView as RNSafeArea } from "react-native-safe-area-context";
+
+const SafeAreaView = styled(RNSafeArea);
const SubscriptionDetails = () => {
const { id } = useLocalSearchParams<{ id: string }>();
return (
-
+
Subscription Details: {id}
{" "}
Go Back{" "}
-
+
);
};
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 1eb8d42..7897504 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,8 +1,29 @@
import "@/global.css";
-import { Stack } from "expo-router";
+import { useFonts } from "expo-font";
+import { SplashScreen, Stack } from "expo-router";
+import { useEffect } from "react";
import { StatusBar } from "react-native";
+SplashScreen.preventAutoHideAsync();
+
export default function RootLayout() {
+ const [fontsLoaded, fontsError] = useFonts({
+ "sans-light": require("../assets/fonts/PlusJakartaSans-Light.ttf"),
+ "sans-regular": require("../assets/fonts/PlusJakartaSans-Regular.ttf"),
+ "sans-medium": require("../assets/fonts/PlusJakartaSans-Medium.ttf"),
+ "sans-semibold": require("../assets/fonts/PlusJakartaSans-SemiBold.ttf"),
+ "sans-bold": require("../assets/fonts/PlusJakartaSans-Bold.ttf"),
+ "sans-extrabold": require("../assets/fonts/PlusJakartaSans-ExtraBold.ttf"),
+ });
+
+ useEffect(() => {
+ if (fontsLoaded || fontsError) {
+ SplashScreen.hideAsync();
+ }
+ }, [fontsLoaded, fontsError]);
+
+ if (!fontsLoaded && !fontsError) return null;
+
return (
<>
diff --git a/assets/images/avatar.png b/assets/images/avatar.png
index f841c71..6ff9daa 100644
Binary files a/assets/images/avatar.png and b/assets/images/avatar.png differ
diff --git a/components/ListHeading.tsx b/components/ListHeading.tsx
new file mode 100644
index 0000000..0b79c90
--- /dev/null
+++ b/components/ListHeading.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { Text, TouchableOpacity, View } from "react-native";
+
+const ListHeading = ({ title, onPress }: ListHeadingProps) => {
+ return (
+
+ {title}
+
+
+ View All
+
+
+ );
+};
+
+export default ListHeading;
diff --git a/components/SubscriptionCard.tsx b/components/SubscriptionCard.tsx
new file mode 100644
index 0000000..fa0bc19
--- /dev/null
+++ b/components/SubscriptionCard.tsx
@@ -0,0 +1,127 @@
+import { formatCurrency } from "@/lib/utils/currency";
+import { formatSubscriptionDateTime } from "@/lib/utils/date";
+import { formatStatusLabel } from "@/lib/utils/status";
+import clsx from "clsx";
+import React from "react";
+import { Image, Pressable, Text, View } from "react-native";
+
+const SubscriptionCard = ({
+ name,
+ price,
+ currency,
+ icon,
+ billing,
+ color,
+ category,
+ plan,
+ paymentMethod,
+ renewalDate,
+ startDate,
+ status,
+ expanded,
+ onPress,
+}: SubscriptionCardProps) => {
+ return (
+
+
+
+
+
+
+ {name}
+
+
+ {category?.trim() ||
+ plan?.trim() ||
+ formatSubscriptionDateTime(renewalDate)}
+
+
+
+
+
+
+ {formatCurrency(price, currency)}
+
+ {billing}
+
+
+
+ {expanded && (
+
+
+
+
+ Payment:
+
+ {paymentMethod?.trim()}
+
+
+
+
+
+
+ Category:
+
+ {category?.trim() || plan?.trim()}
+
+
+
+
+
+
+ Started:
+
+ {startDate ? formatSubscriptionDateTime(startDate) : ""}
+
+
+
+
+
+
+ Renewal Date:
+
+ {renewalDate ? formatSubscriptionDateTime(renewalDate) : ""}
+
+
+
+
+
+
+ Status:
+
+ {status ? formatStatusLabel(status) : ""}
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default SubscriptionCard;
diff --git a/components/UpcomingSubscriptionCard.tsx b/components/UpcomingSubscriptionCard.tsx
new file mode 100644
index 0000000..70de3d7
--- /dev/null
+++ b/components/UpcomingSubscriptionCard.tsx
@@ -0,0 +1,31 @@
+import { formatCurrency } from "@/lib/utils/currency";
+import React from "react";
+import { Image, Text, View } from "react-native";
+
+const UpcomingSubscriptionCard = ({
+ name,
+ price,
+ daysLeft,
+ icon,
+ currency,
+}: UpcomingSubscriptionCardProps) => {
+ return (
+
+
+
+
+ {formatCurrency(price, currency)}
+
+ {daysLeft} {daysLeft > 1 ? "days" : "day"} left
+
+
+
+
+
+ {name}
+
+
+ );
+};
+
+export default UpcomingSubscriptionCard;
diff --git a/constants/data.ts b/constants/data.ts
index 70fa413..3f59690 100644
--- a/constants/data.ts
+++ b/constants/data.ts
@@ -1,8 +1,107 @@
import icons from "./icons";
-export const tabs = [
+export const tabs: AppTab[] = [
{ name: "index", title: "Home", icon: icons.home },
{ name: "subscriptions", title: "Subscriptions", icon: icons.wallet },
{ name: "insights", title: "Insights", icon: icons.activity },
{ name: "settings", title: "Settings", icon: icons.setting },
];
+
+export const HOME_USER = {
+ name: "Ken Cedrick",
+};
+
+export const HOME_BALANCE = {
+ amount: 19972.48,
+ nextRenewalDate: "2026-08-15T09:00:00.000Z",
+};
+
+export const UPCOMING_SUBSCRIPTIONS: UpcomingSubscription[] = [
+ {
+ id: "spotify",
+ icon: icons.spotify,
+ name: "Spotify",
+ price: 5.99,
+ currency: "USD",
+ daysLeft: 2,
+ },
+ {
+ id: "notion",
+ icon: icons.notion,
+ name: "Notion",
+ price: 12.0,
+ currency: "USD",
+ daysLeft: 4,
+ },
+ {
+ id: "figma",
+ icon: icons.figma,
+ name: "Figma",
+ price: 15.0,
+ currency: "USD",
+ daysLeft: 6,
+ },
+];
+
+export const HOME_SUBSCRIPTIONS: Subscription[] = [
+ {
+ id: "adobe-creative-cloud",
+ icon: icons.adobe,
+ name: "Adobe Creative Cloud",
+ plan: "Teams Plan",
+ category: "Design",
+ paymentMethod: "Visa ending in 8530",
+ status: "active",
+ startDate: "2025-03-20T10:00:00.000Z",
+ price: 77.49,
+ currency: "USD",
+ billing: "Monthly",
+ renewalDate: "2026-03-20T10:00:00.000Z",
+ color: "#f5c542",
+ },
+ {
+ id: "github-pro",
+ icon: icons.github,
+ name: "GitHub Pro",
+ plan: "Developer",
+ category: "Developer Tools",
+ paymentMethod: "Mastercard ending in 2408",
+ status: "active",
+ startDate: "2024-11-24T10:00:00.000Z",
+ price: 9.99,
+ currency: "USD",
+ billing: "Monthly",
+ renewalDate: "2026-03-24T10:00:00.000Z",
+ color: "#e8def8",
+ },
+ {
+ id: "claude-pro",
+ icon: icons.claude,
+ name: "Claude Pro",
+ plan: "Pro Plan",
+ category: "AI Tools",
+ paymentMethod: "Amex ending in 1010",
+ status: "paused",
+ startDate: "2025-06-27T10:00:00.000Z",
+ price: 20.0,
+ currency: "USD",
+ billing: "Monthly",
+ renewalDate: "2026-03-27T10:00:00.000Z",
+ color: "#b8d4e3",
+ },
+ {
+ id: "canva-pro",
+ icon: icons.canva,
+ name: "Canva Pro",
+ plan: "Yearly Access",
+ category: "Design",
+ paymentMethod: "Visa ending in 7784",
+ status: "cancelled",
+ startDate: "2024-04-02T10:00:00.000Z",
+ price: 119.99,
+ currency: "USD",
+ billing: "Yearly",
+ renewalDate: "2026-04-02T10:00:00.000Z",
+ color: "#b8e8d0",
+ },
+];
diff --git a/constants/images.ts b/constants/images.ts
new file mode 100644
index 0000000..1d8f5f7
--- /dev/null
+++ b/constants/images.ts
@@ -0,0 +1,7 @@
+import avatar from "@/assets/images/avatar.png";
+import splashpattern from "@/assets/images/splash-pattern.png";
+
+export default {
+ splashpattern,
+ avatar,
+};
diff --git a/global.css b/global.css
index 2218b1a..4a69400 100644
--- a/global.css
+++ b/global.css
@@ -5,416 +5,416 @@
@import "nativewind/theme";
@theme {
- --color-background: #fff9e3;
- --color-foreground: #081126;
- --color-card: #fff8e7;
- --color-muted: #f6eecf;
- --color-muted-foreground: rgba(0, 0, 0, 0.6);
- --color-primary: #081126;
- --color-accent: #ea7a53;
- --color-border: rgba(0, 0, 0, 0.1);
- --color-success: #16a34a;
- --color-destructive: #dc2626;
- --color-subscription: #8fd1bd;
-
- --spacing-0: 0px;
- --spacing-1: 4px;
- --spacing-2: 8px;
- --spacing-3: 12px;
- --spacing-4: 16px;
- --spacing-5: 20px;
- --spacing-6: 24px;
- --spacing-7: 28px;
- --spacing-8: 32px;
- --spacing-9: 36px;
- --spacing-10: 40px;
- --spacing-11: 44px;
- --spacing-12: 48px;
- --spacing-14: 56px;
- --spacing-16: 64px;
- --spacing-18: 72px;
- --spacing-20: 80px;
- --spacing-24: 96px;
- --spacing-30: 120px;
-
- --font-sans: sans-regular;
- --font-sans-light: sans-light;
- --font-sans-medium: sans-medium;
- --font-sans-semibold: sans-semibold;
- --font-sans-bold: sans-bold;
- --font-sans-extrabold: sans-extrabold;
+ --color-background: #fff9e3;
+ --color-foreground: #081126;
+ --color-card: #fff8e7;
+ --color-muted: #f6eecf;
+ --color-muted-foreground: rgba(0, 0, 0, 0.6);
+ --color-primary: #081126;
+ --color-accent: #ea7a53;
+ --color-border: rgba(0, 0, 0, 0.1);
+ --color-success: #16a34a;
+ --color-destructive: #dc2626;
+ --color-subscription: #8fd1bd;
+
+ --spacing-0: 0px;
+ --spacing-1: 4px;
+ --spacing-2: 8px;
+ --spacing-3: 12px;
+ --spacing-4: 16px;
+ --spacing-5: 20px;
+ --spacing-6: 24px;
+ --spacing-7: 28px;
+ --spacing-8: 32px;
+ --spacing-9: 36px;
+ --spacing-10: 40px;
+ --spacing-11: 44px;
+ --spacing-12: 48px;
+ --spacing-14: 56px;
+ --spacing-16: 64px;
+ --spacing-18: 72px;
+ --spacing-20: 80px;
+ --spacing-24: 96px;
+ --spacing-30: 120px;
+
+ --font-sans: sans-regular;
+ --font-sans-light: sans-light;
+ --font-sans-medium: sans-medium;
+ --font-sans-semibold: sans-semibold;
+ --font-sans-bold: sans-bold;
+ --font-sans-extrabold: sans-extrabold;
}
@layer components {
- .tabs-icon {
- @apply size-12 items-center justify-center;
- }
+ .tabs-icon {
+ @apply size-12 items-center justify-center;
+ }
- .tabs-pill {
- @apply size-12 items-center justify-center rounded-full bg-transparent;
- }
+ .tabs-pill {
+ @apply size-12 items-center justify-center rounded-full bg-transparent;
+ }
- .tabs-active {
- @apply bg-accent;
- }
+ .tabs-active {
+ @apply bg-accent;
+ }
- .tabs-glyph {
- @apply size-6;
- }
+ .tabs-glyph {
+ @apply size-6;
+ }
- .home-header {
- @apply mb-2.5 flex-row items-center justify-between;
- }
+ .home-header {
+ @apply mb-2.5 flex-row items-center justify-between;
+ }
- .home-user {
- @apply flex-row items-center;
- }
+ .home-user {
+ @apply flex-row items-center;
+ }
- .home-avatar {
- @apply size-16 rounded-full;
- }
+ .home-avatar {
+ @apply size-16 rounded-full;
+ }
- .home-user-name {
- @apply ml-4 text-2xl font-sans-bold text-primary;
- }
+ .home-user-name {
+ @apply ml-4 text-2xl font-sans-bold text-primary;
+ }
- .home-add-icon {
- @apply size-12;
- }
+ .home-add-icon {
+ @apply size-8;
+ }
- .home-balance-card {
- @apply my-2.5 min-h-50 justify-between gap-5 rounded-bl-4xl rounded-tr-4xl bg-accent p-6;
- }
+ .home-balance-card {
+ @apply my-2.5 min-h-50 justify-between gap-5 rounded-bl-4xl rounded-tr-4xl bg-accent p-6;
+ }
- .home-balance-label {
- @apply text-xl font-sans-semibold text-white/80;
- }
+ .home-balance-label {
+ @apply text-xl font-sans-semibold text-white/80;
+ }
- .home-balance-row {
- @apply flex-row items-center justify-between;
- }
+ .home-balance-row {
+ @apply flex-row items-center justify-between;
+ }
- .home-balance-amount {
- @apply text-4xl font-sans-extrabold text-white;
- }
+ .home-balance-amount {
+ @apply text-4xl font-sans-extrabold text-white;
+ }
- .home-balance-date {
- @apply text-xl font-sans-medium text-white;
- }
+ .home-balance-date {
+ @apply text-xl font-sans-medium text-white;
+ }
- .list-head {
- @apply my-5 flex-row items-center justify-between;
- }
+ .list-head {
+ @apply my-5 flex-row items-center justify-between;
+ }
- .list-title {
- @apply text-2xl font-sans-bold text-primary;
- }
+ .list-title {
+ @apply text-2xl font-sans-bold text-primary;
+ }
- .list-action {
- @apply rounded-full border border-black/20 px-4 py-1;
- }
+ .list-action {
+ @apply rounded-full border border-black/20 px-4 py-1;
+ }
- .list-action-text {
- @apply text-lg font-sans-semibold text-primary;
- }
+ .list-action-text {
+ @apply text-lg font-sans-semibold text-primary;
+ }
- .upcoming-card {
- @apply mr-4 w-44 rounded-2xl border border-black/10 bg-background p-4;
- }
+ .upcoming-card {
+ @apply mr-4 w-44 rounded-2xl border border-black/10 bg-background p-4;
+ }
- .upcoming-row {
- @apply flex-row items-center gap-3;
- }
+ .upcoming-row {
+ @apply flex-row items-center gap-3;
+ }
- .upcoming-icon {
- @apply size-14;
- }
+ .upcoming-icon {
+ @apply size-14;
+ }
- .upcoming-price {
- @apply text-lg font-sans-bold text-primary;
- }
+ .upcoming-price {
+ @apply text-lg font-sans-bold text-primary;
+ }
- .upcoming-meta {
- @apply text-sm font-sans-semibold text-muted-foreground;
- }
+ .upcoming-meta {
+ @apply text-sm font-sans-semibold text-muted-foreground;
+ }
- .upcoming-name {
- @apply mt-2 text-lg font-sans-bold text-primary;
- }
+ .upcoming-name {
+ @apply mt-2 text-lg font-sans-bold text-primary;
+ }
- .sub-card {
- @apply rounded-2xl border border-border p-4;
- }
+ .sub-card {
+ @apply rounded-2xl border border-border p-4;
+ }
- .sub-card-expanded {
- @apply bg-subscription;
- }
+ .sub-card-expanded {
+ @apply bg-subscription;
+ }
- .sub-head {
- @apply flex-row items-center py-2;
- }
+ .sub-head {
+ @apply flex-row items-center py-2;
+ }
- .sub-main {
- @apply min-w-0 flex-1 flex-row items-center gap-3;
- }
+ .sub-main {
+ @apply min-w-0 flex-1 flex-row items-center gap-3;
+ }
- .sub-icon {
- @apply size-16 rounded-lg;
- }
+ .sub-icon {
+ @apply size-16 rounded-lg;
+ }
- .sub-copy {
- @apply min-w-0 flex-1;
- }
+ .sub-copy {
+ @apply min-w-0 flex-1;
+ }
- .sub-title {
- @apply mb-1 text-lg font-sans-bold text-primary;
- }
+ .sub-title {
+ @apply mb-1 text-lg font-sans-bold text-primary;
+ }
- .sub-meta {
- @apply text-sm font-sans-semibold text-muted-foreground;
- }
+ .sub-meta {
+ @apply text-sm font-sans-semibold text-muted-foreground;
+ }
- .sub-price-box {
- @apply ml-3 shrink-0 items-end;
- }
+ .sub-price-box {
+ @apply ml-3 shrink-0 items-end;
+ }
- .sub-price {
- @apply mb-1 text-lg font-sans-bold text-primary;
- }
+ .sub-price {
+ @apply mb-1 text-lg font-sans-bold text-primary;
+ }
- .sub-billing {
- @apply text-sm font-sans-medium text-muted-foreground;
- }
+ .sub-billing {
+ @apply text-sm font-sans-medium text-muted-foreground;
+ }
- .sub-body {
- @apply mt-6 gap-4;
- }
+ .sub-body {
+ @apply mt-6 gap-4;
+ }
- .sub-details {
- @apply gap-6;
- }
+ .sub-details {
+ @apply gap-6;
+ }
- .sub-row {
- @apply flex-row items-center justify-between gap-3;
- }
+ .sub-row {
+ @apply flex-row items-center justify-between gap-3;
+ }
- .sub-row-copy {
- @apply min-w-0 flex-1 flex-row items-center gap-2;
- }
+ .sub-row-copy {
+ @apply min-w-0 flex-1 flex-row items-center gap-2;
+ }
- .sub-label {
- @apply shrink-0 text-base font-sans-medium text-muted-foreground;
- }
+ .sub-label {
+ @apply shrink-0 text-base font-sans-medium text-muted-foreground;
+ }
- .sub-value {
- @apply flex-1 font-sans-bold text-primary;
- }
+ .sub-value {
+ @apply flex-1 font-sans-bold text-primary;
+ }
- .sub-cancel {
- @apply mt-2 items-center rounded-full bg-primary py-4;
- }
+ .sub-cancel {
+ @apply mt-2 items-center rounded-full bg-primary py-4;
+ }
- .sub-cancel-disabled {
- @apply bg-primary/35;
- }
+ .sub-cancel-disabled {
+ @apply bg-primary/35;
+ }
- .sub-cancel-text {
- @apply font-sans-bold text-background;
- }
+ .sub-cancel-text {
+ @apply font-sans-bold text-background;
+ }
- .home-empty-state {
- @apply py-4 text-sm font-sans-medium text-black/60;
- }
+ .home-empty-state {
+ @apply py-4 text-sm font-sans-medium text-black/60;
+ }
- .auth-safe-area {
- @apply flex-1 bg-background;
- }
+ .auth-safe-area {
+ @apply flex-1 bg-background;
+ }
- .auth-screen {
- @apply flex-1 bg-background;
- }
+ .auth-screen {
+ @apply flex-1 bg-background;
+ }
- .auth-scroll {
- @apply flex-1;
- }
+ .auth-scroll {
+ @apply flex-1;
+ }
- .auth-content {
- @apply grow px-5 pb-10 pt-8;
- }
+ .auth-content {
+ @apply grow px-5 pb-10 pt-8;
+ }
- .auth-brand-block {
- @apply mt-2 items-center;
- }
+ .auth-brand-block {
+ @apply mt-2 items-center;
+ }
- .auth-logo-wrap {
- @apply mb-7 flex-row items-center gap-3;
- }
+ .auth-logo-wrap {
+ @apply mb-7 flex-row items-center gap-3;
+ }
- .auth-logo-mark {
- @apply relative size-14 items-center justify-center rounded-2xl bg-accent;
- }
+ .auth-logo-mark {
+ @apply relative size-14 items-center justify-center rounded-2xl bg-accent;
+ }
- .auth-logo-mark-text {
- @apply text-2xl font-sans-extrabold text-background;
- }
+ .auth-logo-mark-text {
+ @apply text-2xl font-sans-extrabold text-background;
+ }
- .auth-wordmark {
- @apply text-3xl font-sans-extrabold text-primary;
- }
+ .auth-wordmark {
+ @apply text-3xl font-sans-extrabold text-primary;
+ }
- .auth-wordmark-sub {
- @apply -mt-1 text-xs font-sans-semibold uppercase tracking-[1px] text-muted-foreground;
- }
+ .auth-wordmark-sub {
+ @apply -mt-1 text-xs font-sans-semibold uppercase tracking-[1px] text-muted-foreground;
+ }
- .auth-title {
- @apply text-3xl font-sans-bold text-primary;
- }
+ .auth-title {
+ @apply text-3xl font-sans-bold text-primary;
+ }
- .auth-subtitle {
- @apply mt-2 max-w-[320px] text-center text-base font-sans-medium text-muted-foreground;
- }
+ .auth-subtitle {
+ @apply mt-2 max-w-[320px] text-center text-base font-sans-medium text-muted-foreground;
+ }
- .auth-card {
- @apply mt-8 rounded-3xl border border-border bg-card p-5;
- }
+ .auth-card {
+ @apply mt-8 rounded-3xl border border-border bg-card p-5;
+ }
- .auth-form {
- @apply gap-4;
- }
+ .auth-form {
+ @apply gap-4;
+ }
- .auth-field {
- @apply gap-2;
- }
+ .auth-field {
+ @apply gap-2;
+ }
- .auth-label {
- @apply text-sm font-sans-semibold text-primary;
- }
+ .auth-label {
+ @apply text-sm font-sans-semibold text-primary;
+ }
- .auth-input {
- @apply rounded-2xl border border-border bg-background px-4 py-4 text-base font-sans-medium text-primary;
- }
+ .auth-input {
+ @apply rounded-2xl border border-border bg-background px-4 py-4 text-base font-sans-medium text-primary;
+ }
- .auth-input-error {
- @apply border-destructive;
- }
+ .auth-input-error {
+ @apply border-destructive;
+ }
- .auth-error {
- @apply text-xs font-sans-medium text-destructive;
- }
+ .auth-error {
+ @apply text-xs font-sans-medium text-destructive;
+ }
- .auth-helper {
- @apply text-sm font-sans-medium text-muted-foreground;
- }
+ .auth-helper {
+ @apply text-sm font-sans-medium text-muted-foreground;
+ }
- .auth-button {
- @apply mt-1 items-center rounded-2xl bg-accent py-4;
- }
+ .auth-button {
+ @apply mt-1 items-center rounded-2xl bg-accent py-4;
+ }
- .auth-button-disabled {
- @apply bg-accent/45;
- }
+ .auth-button-disabled {
+ @apply bg-accent/45;
+ }
- .auth-button-text {
- @apply text-base font-sans-bold text-primary;
- }
+ .auth-button-text {
+ @apply text-base font-sans-bold text-primary;
+ }
- .auth-secondary-button {
- @apply items-center rounded-2xl border border-accent/30 bg-accent/10 py-3;
- }
+ .auth-secondary-button {
+ @apply items-center rounded-2xl border border-accent/30 bg-accent/10 py-3;
+ }
- .auth-secondary-button-text {
- @apply text-sm font-sans-semibold text-accent;
- }
+ .auth-secondary-button-text {
+ @apply text-sm font-sans-semibold text-accent;
+ }
- .auth-divider-row {
- @apply my-4 flex-row items-center gap-3;
- }
+ .auth-divider-row {
+ @apply my-4 flex-row items-center gap-3;
+ }
- .auth-divider-line {
- @apply h-px flex-1 bg-border;
- }
+ .auth-divider-line {
+ @apply h-px flex-1 bg-border;
+ }
- .auth-divider-text {
- @apply text-[11px] font-sans-semibold uppercase tracking-[1px] text-muted-foreground;
- }
+ .auth-divider-text {
+ @apply text-[11px] font-sans-semibold uppercase tracking-[1px] text-muted-foreground;
+ }
- .auth-link-row {
- @apply mt-5 flex-row items-center justify-center gap-1;
- }
+ .auth-link-row {
+ @apply mt-5 flex-row items-center justify-center gap-1;
+ }
- .auth-link-copy {
- @apply text-sm font-sans-medium text-muted-foreground;
- }
+ .auth-link-copy {
+ @apply text-sm font-sans-medium text-muted-foreground;
+ }
- .auth-link {
- @apply text-sm font-sans-bold text-accent;
- }
+ .auth-link {
+ @apply text-sm font-sans-bold text-accent;
+ }
- .modal-overlay {
- @apply flex-1 bg-black/50;
- }
+ .modal-overlay {
+ @apply flex-1 bg-black/50;
+ }
- .modal-container {
- @apply mt-auto max-h-[85%] rounded-t-3xl bg-background;
- }
+ .modal-container {
+ @apply mt-auto max-h-[85%] rounded-t-3xl bg-background;
+ }
- .modal-header {
- @apply flex-row items-center justify-between border-b border-border px-5 py-4;
- }
+ .modal-header {
+ @apply flex-row items-center justify-between border-b border-border px-5 py-4;
+ }
- .modal-title {
- @apply text-xl font-sans-bold text-primary;
- }
+ .modal-title {
+ @apply text-xl font-sans-bold text-primary;
+ }
- .modal-close {
- @apply size-8 items-center justify-center rounded-full bg-muted;
- }
+ .modal-close {
+ @apply size-8 items-center justify-center rounded-full bg-muted;
+ }
- .modal-close-text {
- @apply text-lg font-sans-bold text-primary;
- }
+ .modal-close-text {
+ @apply text-lg font-sans-bold text-primary;
+ }
- .modal-body {
- @apply gap-5 p-5;
- }
+ .modal-body {
+ @apply gap-5 p-5;
+ }
- .picker-row {
- @apply flex-row gap-3;
- }
+ .picker-row {
+ @apply flex-row gap-3;
+ }
- .picker-option {
- @apply flex-1 items-center rounded-2xl border border-border bg-background py-3;
- }
+ .picker-option {
+ @apply flex-1 items-center rounded-2xl border border-border bg-background py-3;
+ }
- .picker-option-active {
- @apply border-accent bg-accent/10;
- }
+ .picker-option-active {
+ @apply border-accent bg-accent/10;
+ }
- .picker-option-text {
- @apply text-sm font-sans-semibold text-muted-foreground;
- }
+ .picker-option-text {
+ @apply text-sm font-sans-semibold text-muted-foreground;
+ }
- .picker-option-text-active {
- @apply text-accent;
- }
+ .picker-option-text-active {
+ @apply text-accent;
+ }
- .category-scroll {
- @apply flex-row flex-wrap gap-2;
- }
+ .category-scroll {
+ @apply flex-row flex-wrap gap-2;
+ }
- .category-chip {
- @apply rounded-full border border-border bg-background px-4 py-2;
- }
+ .category-chip {
+ @apply rounded-full border border-border bg-background px-4 py-2;
+ }
- .category-chip-active {
- @apply border-accent bg-accent/10;
- }
+ .category-chip-active {
+ @apply border-accent bg-accent/10;
+ }
- .category-chip-text {
- @apply text-sm font-sans-semibold text-muted-foreground;
- }
+ .category-chip-text {
+ @apply text-sm font-sans-semibold text-muted-foreground;
+ }
- .category-chip-text-active {
- @apply text-accent;
- }
-}
\ No newline at end of file
+ .category-chip-text-active {
+ @apply text-accent;
+ }
+}
diff --git a/lib/utils/currency.ts b/lib/utils/currency.ts
new file mode 100644
index 0000000..8d41cb8
--- /dev/null
+++ b/lib/utils/currency.ts
@@ -0,0 +1,60 @@
+const API_URL = "https://api.frankfurter.app/latest";
+
+const STATIC_RATES: Record = {
+ USD: 0.017,
+ EUR: 0.016,
+ GBP: 0.014,
+ JPY: 2.63,
+ AUD: 0.026,
+ CAD: 0.024,
+ SGD: 0.023,
+};
+
+let cachedRates: Record | null = null;
+let lastFetched = 0;
+const CACHE_TTL = 30 * 60 * 1000;
+
+async function fetchRates(): Promise> {
+ const res = await fetch(`${API_URL}?from=PHP`);
+ const data = await res.json();
+ return data.rates as Record;
+}
+
+export async function initCurrencyRates(): Promise {
+ try {
+ cachedRates = await fetchRates();
+ lastFetched = Date.now();
+ } catch {
+ cachedRates = STATIC_RATES;
+ }
+}
+
+function getRate(fromCurrency: string): number {
+ const now = Date.now();
+ if (!cachedRates || now - lastFetched > CACHE_TTL) {
+ return STATIC_RATES[fromCurrency] ?? 1;
+ }
+ return cachedRates[fromCurrency] ?? STATIC_RATES[fromCurrency] ?? 1;
+}
+
+export function formatCurrency(
+ value: number,
+ fromCurrency: string = "PHP",
+): string {
+ try {
+ let phpValue = value;
+
+ if (fromCurrency !== "PHP") {
+ phpValue = value / getRate(fromCurrency);
+ }
+
+ return new Intl.NumberFormat("en-PH", {
+ style: "currency",
+ currency: "PHP",
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }).format(phpValue);
+ } catch {
+ return `₱${value.toFixed(2)}`;
+ }
+}
diff --git a/lib/utils/date.ts b/lib/utils/date.ts
new file mode 100644
index 0000000..c4a4815
--- /dev/null
+++ b/lib/utils/date.ts
@@ -0,0 +1,9 @@
+import dayjs from "dayjs";
+
+export const formatSubscriptionDateTime = (value?: string): string => {
+ if (!value) return "Not provided";
+ const parsedDate = dayjs(value);
+ return parsedDate.isValid()
+ ? parsedDate.format("MM/DD/YYYY")
+ : "Not provided";
+};
diff --git a/lib/utils/status.ts b/lib/utils/status.ts
new file mode 100644
index 0000000..7afb0df
--- /dev/null
+++ b/lib/utils/status.ts
@@ -0,0 +1,4 @@
+export const formatStatusLabel = (value?: string): string => {
+ if (!value) return "Unknown";
+ return value.charAt(0).toUpperCase() + value.slice(1);
+};
diff --git a/package-lock.json b/package-lock.json
index fe31480..4eb1b81 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"clsx": "^2.1.1",
+ "dayjs": "^1.11.21",
"expo": "~54.0.34",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
@@ -5932,6 +5933,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dayjs": {
+ "version": "1.11.21",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+ "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
diff --git a/package.json b/package.json
index 54b450d..bf41afe 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"clsx": "^2.1.1",
+ "dayjs": "^1.11.21",
"expo": "~54.0.34",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
diff --git a/type.d.ts b/type.d.ts
index 58c2ad7..ac0a9b2 100644
--- a/type.d.ts
+++ b/type.d.ts
@@ -1,8 +1,60 @@
-import { ImageSourcePropType } from "react-native";
+import type { ImageSourcePropType } from "react-native";
declare global {
+ interface AppTab {
+ name: string;
+ title: string;
+ icon: ImageSourcePropType;
+ }
+
interface TabIconProps {
focused: boolean;
icon: ImageSourcePropType;
}
+
+ interface Subscription {
+ id: string;
+ icon: ImageSourcePropType;
+ name: string;
+ plan?: string;
+ category?: string;
+ paymentMethod?: string;
+ status?: string;
+ startDate?: string;
+ price: number;
+ currency?: string;
+ billing: string;
+ frequency?: string;
+ renewalDate?: string;
+ color?: string;
+ }
+
+ interface SubscriptionCardProps extends Omit {
+ expanded: boolean;
+ onPress: () => void;
+ onCancelPress?: () => void;
+ isCancelling?: boolean;
+ }
+
+ interface UpcomingSubscription {
+ id: string;
+ icon: ImageSourcePropType;
+ name: string;
+ price: number;
+ currency?: string;
+ daysLeft: number;
+ }
+
+ interface UpcomingSubscriptionCardProps extends Omit<
+ UpcomingSubscription,
+ "id"
+ > {}
+
+ interface ListHeadingProps {
+ title: string;
+ onPress?: () => void;
+ }
}
+
+export { };
+