@umituz/react-native-subscription 2.27.114 → 2.27.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/infrastructure/CreditsRepository.ts +16 -19
  3. package/src/domains/credits/utils/creditCalculations.ts +6 -11
  4. package/src/domains/subscription/application/SubscriptionSyncService.ts +3 -0
  5. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +3 -0
  6. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -0
  7. package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +1 -0
  8. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -0
  9. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +4 -8
  10. package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +18 -42
  11. package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +2 -6
  12. package/src/shared/infrastructure/SubscriptionEventBus.ts +1 -0
  13. package/src/shared/infrastructure/firestore/collectionUtils.ts +67 -0
  14. package/src/shared/infrastructure/firestore/index.ts +6 -0
  15. package/src/shared/infrastructure/firestore/resultUtils.ts +68 -0
  16. package/src/shared/infrastructure/index.ts +6 -0
  17. package/src/shared/presentation/hooks/index.ts +6 -0
  18. package/src/shared/presentation/hooks/useAsyncState.ts +72 -0
  19. package/src/shared/presentation/hooks/useServiceCall.ts +66 -0
  20. package/src/shared/types/CommonTypes.ts +6 -1
  21. package/src/shared/types/ReactTypes.ts +80 -0
  22. package/src/shared/utils/Result.ts +0 -16
  23. package/src/shared/utils/arrayUtils.core.ts +81 -0
  24. package/src/shared/utils/arrayUtils.query.ts +118 -0
  25. package/src/shared/utils/arrayUtils.transforms.ts +116 -0
  26. package/src/shared/utils/arrayUtils.ts +19 -0
  27. package/src/shared/utils/index.ts +14 -0
  28. package/src/shared/utils/numberUtils.aggregate.ts +35 -0
  29. package/src/shared/utils/numberUtils.core.ts +73 -0
  30. package/src/shared/utils/numberUtils.format.ts +42 -0
  31. package/src/shared/utils/numberUtils.math.ts +48 -0
  32. package/src/shared/utils/numberUtils.ts +9 -0
  33. package/src/shared/utils/stringUtils.case.ts +64 -0
  34. package/src/shared/utils/stringUtils.check.ts +65 -0
  35. package/src/shared/utils/stringUtils.format.ts +84 -0
  36. package/src/shared/utils/stringUtils.generate.ts +47 -0
  37. package/src/shared/utils/stringUtils.modify.ts +67 -0
  38. package/src/shared/utils/stringUtils.ts +10 -0
  39. package/src/shared/utils/validators.ts +187 -0
  40. package/src/utils/dateUtils.compare.ts +65 -0
  41. package/src/utils/dateUtils.core.ts +67 -0
  42. package/src/utils/dateUtils.format.ts +138 -0
  43. package/src/utils/dateUtils.math.ts +112 -0
  44. package/src/utils/dateUtils.ts +6 -28
@@ -0,0 +1,64 @@
1
+ /**
2
+ * String Utilities - Case Conversion
3
+ * String case transformation functions
4
+ */
5
+
6
+ /**
7
+ * Capitalize first letter of a string
8
+ */
9
+ export function capitalize(str: string): string {
10
+ if (!str) return "";
11
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
12
+ }
13
+
14
+ /**
15
+ * Capitalize first letter of each word
16
+ */
17
+ export function titleCase(str: string): string {
18
+ if (!str) return "";
19
+ return str
20
+ .toLowerCase()
21
+ .split(" ")
22
+ .map((word) => capitalize(word))
23
+ .join(" ");
24
+ }
25
+
26
+ /**
27
+ * Convert string to kebab-case
28
+ */
29
+ export function kebabCase(str: string): string {
30
+ if (!str) return "";
31
+ return str
32
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
33
+ .replace(/[\s_]+/g, "-")
34
+ .toLowerCase();
35
+ }
36
+
37
+ /**
38
+ * Convert string to snake_case
39
+ */
40
+ export function snakeCase(str: string): string {
41
+ if (!str) return "";
42
+ return str
43
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
44
+ .replace(/[\s-]+/g, "_")
45
+ .toLowerCase();
46
+ }
47
+
48
+ /**
49
+ * Convert string to camelCase
50
+ */
51
+ export function camelCase(str: string): string {
52
+ if (!str) return "";
53
+ return str
54
+ .replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ""))
55
+ .replace(/^[A-Z]/, (char) => char.toLowerCase());
56
+ }
57
+
58
+ /**
59
+ * Convert string to PascalCase
60
+ */
61
+ export function pascalCase(str: string): string {
62
+ if (!str) return "";
63
+ return capitalize(camelCase(str));
64
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * String Utilities - Check Operations
3
+ * String validation and comparison functions
4
+ */
5
+
6
+ /**
7
+ * Check if value is a non-empty string
8
+ */
9
+ function isNonEmptyString(value: unknown): value is string {
10
+ return typeof value === "string" && value.trim().length > 0;
11
+ }
12
+
13
+ /**
14
+ * Check if string contains a substring (case insensitive)
15
+ */
16
+ export function containsIgnoreCase(str: string, search: string): boolean {
17
+ return str.toLowerCase().includes(search.toLowerCase());
18
+ }
19
+
20
+ /**
21
+ * Check if string starts with a prefix (case insensitive)
22
+ */
23
+ export function startsWithIgnoreCase(str: string, prefix: string): boolean {
24
+ return str.toLowerCase().startsWith(prefix.toLowerCase());
25
+ }
26
+
27
+ /**
28
+ * Check if string ends with a suffix (case insensitive)
29
+ */
30
+ export function endsWithIgnoreCase(str: string, suffix: string): boolean {
31
+ return str.toLowerCase().endsWith(suffix.toLowerCase());
32
+ }
33
+
34
+ /**
35
+ * Check if a string is a valid email (basic validation)
36
+ */
37
+ export function isValidEmailString(value: unknown): value is string {
38
+ if (!isNonEmptyString(value)) {
39
+ return false;
40
+ }
41
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
42
+ return emailRegex.test(value);
43
+ }
44
+
45
+ /**
46
+ * Check if value is a valid URL (basic validation)
47
+ */
48
+ export function isValidUrlString(value: unknown): value is string {
49
+ if (!isNonEmptyString(value)) {
50
+ return false;
51
+ }
52
+ try {
53
+ new URL(value as string);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Check if a string is a valid product identifier
62
+ */
63
+ export function isValidProductIdString(value: unknown): value is string {
64
+ return isNonEmptyString(value) && /^[a-zA-Z0-9._-]+$/.test(value as string);
65
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * String Utilities - Formatting
3
+ * String formatting and display functions
4
+ */
5
+
6
+ /**
7
+ * Truncate string to specified length and add ellipsis
8
+ */
9
+ export function truncate(str: string, maxLength: number, suffix: string = "..."): string {
10
+ if (!str || str.length <= maxLength) return str;
11
+ return str.slice(0, maxLength - suffix.length) + suffix;
12
+ }
13
+
14
+ /**
15
+ * Format bytes as human readable string
16
+ */
17
+ export function formatBytes(bytes: number, decimals: number = 2): string {
18
+ if (bytes === 0) return "0 Bytes";
19
+
20
+ const k = 1024;
21
+ const dm = decimals < 0 ? 0 : decimals;
22
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
23
+
24
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
25
+
26
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
27
+ }
28
+
29
+ /**
30
+ * Format a list of items as a comma-separated string with "and" for the last item
31
+ */
32
+ export function formatList(items: string[], conjunction: string = "and"): string {
33
+ if (items.length === 0) return "";
34
+ if (items.length === 1) return items[0] ?? "";
35
+ if (items.length === 2) return items.join(` ${conjunction} `);
36
+
37
+ const lastItem = items[items.length - 1];
38
+ return `${items.slice(0, -1).join(", ")} ${conjunction} ${lastItem ?? ""}`;
39
+ }
40
+
41
+ /**
42
+ * Pluralize a word based on count
43
+ */
44
+ export function pluralize(count: number, singular: string, plural?: string): string {
45
+ if (count === 1) return singular;
46
+ return plural || singular + "s";
47
+ }
48
+
49
+ /**
50
+ * Format number as ordinal (1st, 2nd, 3rd, etc.)
51
+ */
52
+ export function formatOrdinal(num: number): string {
53
+ const suffixes = ["th", "st", "nd", "rd"];
54
+ const value = num % 100;
55
+ const suffix = suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0];
56
+ return `${num}${suffix}`;
57
+ }
58
+
59
+ /**
60
+ * Mask sensitive data (e.g., credit card, email)
61
+ */
62
+ export function maskString(str: string, visibleChars: number = 4, maskChar: string = "*"): string {
63
+ if (!str) return "";
64
+ if (str.length <= visibleChars * 2) return maskChar.repeat(str.length);
65
+
66
+ const start = str.substring(0, visibleChars);
67
+ const end = str.substring(str.length - visibleChars);
68
+ const middleLength = str.length - visibleChars * 2;
69
+
70
+ return `${start}${maskChar.repeat(middleLength)}${end}`;
71
+ }
72
+
73
+ /**
74
+ * Extract initials from a name
75
+ */
76
+ export function getInitials(name: string, maxInitials: number = 2): string {
77
+ if (!name) return "";
78
+ return name
79
+ .split(" ")
80
+ .filter((word) => word.length > 0)
81
+ .map((word) => word.charAt(0).toUpperCase())
82
+ .slice(0, maxInitials)
83
+ .join("");
84
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * String Utilities - Generate Functions
3
+ * String generation and encoding functions
4
+ */
5
+
6
+ /**
7
+ * Generate a random string of specified length
8
+ */
9
+ export function randomString(
10
+ length: number,
11
+ charset: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
12
+ ): string {
13
+ let result = "";
14
+ for (let i = 0; i < length; i++) {
15
+ result += charset.charAt(Math.floor(Math.random() * charset.length));
16
+ }
17
+ return result;
18
+ }
19
+
20
+ /**
21
+ * Generate a random ID
22
+ */
23
+ export function randomId(prefix: string = ""): string {
24
+ const timestamp = Date.now().toString(36);
25
+ const random = Math.random().toString(36).substring(2, 9);
26
+ return prefix ? `${prefix}_${timestamp}${random}` : `${timestamp}${random}`;
27
+ }
28
+
29
+ /**
30
+ * Convert a string to base64
31
+ */
32
+ export function toBase64(str: string): string {
33
+ if (typeof btoa !== "undefined") {
34
+ return btoa(str);
35
+ }
36
+ return Buffer.from(str).toString("base64");
37
+ }
38
+
39
+ /**
40
+ * Decode a base64 string
41
+ */
42
+ export function fromBase64(base64: string): string {
43
+ if (typeof atob !== "undefined") {
44
+ return atob(base64);
45
+ }
46
+ return Buffer.from(base64, "base64").toString();
47
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * String Utilities - Modify Operations
3
+ * String manipulation and transformation functions
4
+ */
5
+
6
+ /**
7
+ * Remove all whitespace from string
8
+ */
9
+ export function removeWhitespace(str: string): string {
10
+ return str.replace(/\s+/g, "");
11
+ }
12
+
13
+ /**
14
+ * Normalize whitespace (replace multiple spaces with single space)
15
+ */
16
+ export function normalizeWhitespace(str: string): string {
17
+ return str.trim().replace(/\s+/g, " ");
18
+ }
19
+
20
+ /**
21
+ * Count words in a string
22
+ */
23
+ export function countWords(str: string): number {
24
+ if (!str.trim()) return 0;
25
+ return str.trim().split(/\s+/).length;
26
+ }
27
+
28
+ /**
29
+ * Count characters in a string (excluding whitespace)
30
+ */
31
+ export function countChars(str: string, excludeWhitespace: boolean = true): number {
32
+ return excludeWhitespace ? removeWhitespace(str).length : str.length;
33
+ }
34
+
35
+ /**
36
+ * Replace all occurrences of a substring
37
+ */
38
+ export function replaceAll(str: string, search: string, replacement: string): string {
39
+ return str.split(search).join(replacement);
40
+ }
41
+
42
+ /**
43
+ * Escape special regex characters in a string
44
+ */
45
+ export function escapeRegex(str: string): string {
46
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ }
48
+
49
+ /**
50
+ * Strip HTML tags from a string
51
+ */
52
+ export function stripHtml(html: string): string {
53
+ return html.replace(/<[^>]*>/g, "");
54
+ }
55
+
56
+ /**
57
+ * Sanitize string input (trim and remove extra whitespace)
58
+ */
59
+ export function sanitizeStringValue(value: unknown): string | null {
60
+ if (value === null || value === undefined) {
61
+ return null;
62
+ }
63
+ if (typeof value === "string") {
64
+ return value.trim().replace(/\s+/g, " ");
65
+ }
66
+ return String(value).trim().replace(/\s+/g, " ");
67
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * String Utilities
3
+ * Re-exports all string utility modules
4
+ */
5
+
6
+ export * from "./stringUtils.case";
7
+ export * from "./stringUtils.check";
8
+ export * from "./stringUtils.format";
9
+ export * from "./stringUtils.generate";
10
+ export * from "./stringUtils.modify";
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Shared Validation Utilities
3
+ * Common validation functions and type guards
4
+ */
5
+
6
+ /**
7
+ * Check if value is a non-empty string
8
+ */
9
+ export function isNonEmptyString(value: unknown): value is string {
10
+ return typeof value === "string" && value.trim().length > 0;
11
+ }
12
+
13
+ /**
14
+ * Check if value is a valid number (not NaN or Infinity)
15
+ */
16
+ export function isValidNumber(value: unknown): value is number {
17
+ return typeof value === "number" && !isNaN(value) && isFinite(value);
18
+ }
19
+
20
+ /**
21
+ * Check if value is a positive number
22
+ */
23
+ export function isPositiveNumber(value: unknown): value is number {
24
+ return isValidNumber(value) && value > 0;
25
+ }
26
+
27
+ /**
28
+ * Check if value is a non-negative number
29
+ */
30
+ export function isNonNegativeNumber(value: unknown): value is number {
31
+ return isValidNumber(value) && value >= 0;
32
+ }
33
+
34
+ /**
35
+ * Check if value is a valid integer
36
+ */
37
+ export function isInteger(value: unknown): value is number {
38
+ return isValidNumber(value) && Number.isInteger(value);
39
+ }
40
+
41
+ /**
42
+ * Check if value is a valid date
43
+ */
44
+ export function isValidDate(value: unknown): boolean {
45
+ if (value instanceof Date) {
46
+ return !isNaN(value.getTime());
47
+ }
48
+ if (typeof value === "string" || typeof value === "number") {
49
+ const d = new Date(value);
50
+ return !isNaN(d.getTime());
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Check if value is a valid email (basic validation)
57
+ */
58
+ export function isValidEmail(value: unknown): value is string {
59
+ if (!isNonEmptyString(value)) {
60
+ return false;
61
+ }
62
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
63
+ return emailRegex.test(value);
64
+ }
65
+
66
+ /**
67
+ * Check if value is a valid URL (basic validation)
68
+ */
69
+ export function isValidUrl(value: unknown): value is string {
70
+ if (!isNonEmptyString(value)) {
71
+ return false;
72
+ }
73
+ try {
74
+ new URL(value);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Check if value is within a numeric range (type guard version)
83
+ */
84
+ export function isValueInRange(
85
+ value: unknown,
86
+ min: number,
87
+ max: number,
88
+ inclusive: boolean = true
89
+ ): value is number {
90
+ if (!isValidNumber(value)) {
91
+ return false;
92
+ }
93
+ if (inclusive) {
94
+ return value >= min && value <= max;
95
+ }
96
+ return value > min && value < max;
97
+ }
98
+
99
+ /**
100
+ * Check if array has at least one element
101
+ */
102
+ export function isNonEmptyArray<T>(value: unknown): value is [T, ...T[]] {
103
+ return Array.isArray(value) && value.length > 0;
104
+ }
105
+
106
+ /**
107
+ * Check if value is a plain object (not null, not array)
108
+ */
109
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
110
+ return typeof value === "object" && value !== null && !Array.isArray(value);
111
+ }
112
+
113
+ /**
114
+ * Check if value is defined (not null or undefined)
115
+ */
116
+ export function isDefined<T>(value: T | null | undefined): value is T {
117
+ return value !== null && value !== undefined;
118
+ }
119
+
120
+ /**
121
+ * Validate that a number is within credit limits
122
+ */
123
+ export function isValidCreditAmount(value: unknown): value is number {
124
+ return isInteger(value) && isNonNegativeNumber(value);
125
+ }
126
+
127
+ /**
128
+ * Validate that a price is valid (positive number with max 2 decimal places)
129
+ */
130
+ export function isValidPrice(value: unknown): value is number {
131
+ if (!isPositiveNumber(value)) {
132
+ return false;
133
+ }
134
+ // Check for max 2 decimal places
135
+ const decimalPlaces = (value.toString().split(".")[1] || "").length;
136
+ return decimalPlaces <= 2;
137
+ }
138
+
139
+ /**
140
+ * Check if a string is a valid product identifier
141
+ */
142
+ export function isValidProductId(value: unknown): value is string {
143
+ return isNonEmptyString(value) && /^[a-zA-Z0-9._-]+$/.test(value);
144
+ }
145
+
146
+ /**
147
+ * Check if value is a valid user ID
148
+ */
149
+ export function isValidUserId(value: unknown): value is string {
150
+ return isNonEmptyString(value) && value.length > 0;
151
+ }
152
+
153
+ /**
154
+ * Sanitize string input (trim and remove extra whitespace)
155
+ */
156
+ export function sanitizeString(value: unknown): string | null {
157
+ if (value === null || value === undefined) {
158
+ return null;
159
+ }
160
+ if (typeof value === "string") {
161
+ return value.trim().replace(/\s+/g, " ");
162
+ }
163
+ return String(value).trim().replace(/\s+/g, " ");
164
+ }
165
+
166
+ /**
167
+ * Validate and sanitize a number input
168
+ */
169
+ export function sanitizeNumber(
170
+ value: unknown,
171
+ defaultValue: number = 0
172
+ ): number {
173
+ if (isValidNumber(value)) {
174
+ return value;
175
+ }
176
+ return defaultValue;
177
+ }
178
+
179
+ /**
180
+ * Check if a value is within an allowed set of values
181
+ */
182
+ export function isOneOf<T>(
183
+ value: unknown,
184
+ allowedValues: readonly T[]
185
+ ): value is T {
186
+ return allowedValues.includes(value as T);
187
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Date Utilities - Comparison Operations
3
+ * Date comparison and calculation functions
4
+ */
5
+
6
+ import { isValidDate, isPast, currentDate } from "./dateUtils.core";
7
+ import { addDays } from "./dateUtils.math";
8
+
9
+ /**
10
+ * Calculate difference in days between two dates
11
+ */
12
+ export function daysBetween(date1: Date | string | number, date2: Date | string | number): number {
13
+ const d1 = new Date(date1);
14
+ const d2 = new Date(date2);
15
+ const diffTime = d2.getTime() - d1.getTime();
16
+ return Math.floor(diffTime / (1000 * 60 * 60 * 24));
17
+ }
18
+
19
+ /**
20
+ * Calculate days remaining until a date
21
+ * Returns null if date is in the past or invalid
22
+ */
23
+ export function daysUntil(date: Date | string | number): number | null {
24
+ const target = new Date(date);
25
+ if (!isValidDate(target) || isPast(target)) {
26
+ return null;
27
+ }
28
+ return daysBetween(currentDate(), target);
29
+ }
30
+
31
+ /**
32
+ * Check if two dates are the same day
33
+ */
34
+ export function isSameDay(date1: Date | string | number, date2: Date | string | number): boolean {
35
+ const d1 = new Date(date1);
36
+ const d2 = new Date(date2);
37
+ return (
38
+ d1.getFullYear() === d2.getFullYear() &&
39
+ d1.getMonth() === d2.getMonth() &&
40
+ d1.getDate() === d2.getDate()
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Check if a date is today
46
+ */
47
+ export function isToday(date: Date | string | number): boolean {
48
+ return isSameDay(date, currentDate());
49
+ }
50
+
51
+ /**
52
+ * Check if a date is yesterday
53
+ */
54
+ export function isYesterday(date: Date | string | number): boolean {
55
+ const yesterday = addDays(currentDate(), -1);
56
+ return isSameDay(date, yesterday);
57
+ }
58
+
59
+ /**
60
+ * Check if a date is tomorrow
61
+ */
62
+ export function isTomorrow(date: Date | string | number): boolean {
63
+ const tomorrow = addDays(currentDate(), 1);
64
+ return isSameDay(date, tomorrow);
65
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Date Utilities - Core Operations
3
+ * Basic date manipulation and validation functions
4
+ */
5
+
6
+ export type DateLike = Date | string | number;
7
+
8
+ /**
9
+ * Checks if a date is in the past
10
+ */
11
+ export function isPast(date: DateLike): boolean {
12
+ const d = new Date(date);
13
+ return d.getTime() < Date.now();
14
+ }
15
+
16
+ /**
17
+ * Checks if a date is in the future
18
+ */
19
+ export function isFuture(date: DateLike): boolean {
20
+ const d = new Date(date);
21
+ return d.getTime() > Date.now();
22
+ }
23
+
24
+ /**
25
+ * Checks if a date is valid
26
+ */
27
+ export function isValidDate(date: DateLike): boolean {
28
+ const d = date instanceof Date ? date : new Date(date);
29
+ return !isNaN(d.getTime());
30
+ }
31
+
32
+ /**
33
+ * Converts various timestamp formats to a safe Date object
34
+ */
35
+ export function toSafeDate(ts: unknown): Date | null {
36
+ if (!ts) return null;
37
+ if (typeof ts === "object" && ts !== null && "toDate" in ts && typeof ts.toDate === "function") {
38
+ return (ts as { toDate: () => Date }).toDate();
39
+ }
40
+ if (ts instanceof Date) return ts;
41
+ if (typeof ts === "string" || typeof ts === "number") {
42
+ const d = new Date(ts);
43
+ return isNaN(d.getTime()) ? null : d;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /**
49
+ * Formats a date to ISO string safely
50
+ */
51
+ export function formatISO(date: Date | null): string | null {
52
+ return date ? date.toISOString() : null;
53
+ }
54
+
55
+ /**
56
+ * Get current timestamp in milliseconds
57
+ */
58
+ export function now(): number {
59
+ return Date.now();
60
+ }
61
+
62
+ /**
63
+ * Get current date
64
+ */
65
+ export function currentDate(): Date {
66
+ return new Date();
67
+ }