enh
This commit is contained in:
parent
8ea26f5705
commit
4199ff7d9b
17 changed files with 605 additions and 196 deletions
83
apps/admin-ui/.detoxrc.js
Normal file
83
apps/admin-ui/.detoxrc.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
/** @type {Detox.DetoxConfig} */
|
||||||
|
module.exports = {
|
||||||
|
testRunner: {
|
||||||
|
args: {
|
||||||
|
'$0': 'jest',
|
||||||
|
config: 'e2e/jest.config.js'
|
||||||
|
},
|
||||||
|
jest: {
|
||||||
|
setupTimeout: 120000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
apps: {
|
||||||
|
'ios.debug': {
|
||||||
|
type: 'ios.app',
|
||||||
|
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YOUR_APP.app',
|
||||||
|
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
|
||||||
|
},
|
||||||
|
'ios.release': {
|
||||||
|
type: 'ios.app',
|
||||||
|
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app',
|
||||||
|
build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
|
||||||
|
},
|
||||||
|
'android.debug': {
|
||||||
|
type: 'android.apk',
|
||||||
|
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
||||||
|
build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
|
||||||
|
reversePorts: [
|
||||||
|
8081
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'android.release': {
|
||||||
|
type: 'android.apk',
|
||||||
|
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
|
||||||
|
build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
devices: {
|
||||||
|
simulator: {
|
||||||
|
type: 'ios.simulator',
|
||||||
|
device: {
|
||||||
|
type: 'iPhone 15'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attached: {
|
||||||
|
type: 'android.attached',
|
||||||
|
device: {
|
||||||
|
adbName: '.*'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emulator: {
|
||||||
|
type: 'android.emulator',
|
||||||
|
device: {
|
||||||
|
avdName: 'Pixel_3a_API_30_x86'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurations: {
|
||||||
|
'ios.sim.debug': {
|
||||||
|
device: 'simulator',
|
||||||
|
app: 'ios.debug'
|
||||||
|
},
|
||||||
|
'ios.sim.release': {
|
||||||
|
device: 'simulator',
|
||||||
|
app: 'ios.release'
|
||||||
|
},
|
||||||
|
'android.att.debug': {
|
||||||
|
device: 'attached',
|
||||||
|
app: 'android.debug'
|
||||||
|
},
|
||||||
|
'android.att.release': {
|
||||||
|
device: 'attached',
|
||||||
|
app: 'android.release'
|
||||||
|
},
|
||||||
|
'android.emu.debug': {
|
||||||
|
device: 'emulator',
|
||||||
|
app: 'android.debug'
|
||||||
|
},
|
||||||
|
'android.emu.release': {
|
||||||
|
device: 'emulator',
|
||||||
|
app: 'android.release'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
12
apps/admin-ui/e2e/jest.config.js
Normal file
12
apps/admin-ui/e2e/jest.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||||
|
module.exports = {
|
||||||
|
rootDir: '..',
|
||||||
|
testMatch: ['<rootDir>/e2e/**/*.test.js'],
|
||||||
|
testTimeout: 120000,
|
||||||
|
maxWorkers: 1,
|
||||||
|
globalSetup: 'detox/runners/jest/globalSetup',
|
||||||
|
globalTeardown: 'detox/runners/jest/globalTeardown',
|
||||||
|
reporters: ['detox/runners/jest/reporter'],
|
||||||
|
testEnvironment: 'detox/runners/jest/testEnvironment',
|
||||||
|
verbose: true,
|
||||||
|
};
|
||||||
23
apps/admin-ui/e2e/starter.test.js
Normal file
23
apps/admin-ui/e2e/starter.test.js
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
describe('Example', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await device.launchApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await device.reloadReactNative();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have welcome screen', async () => {
|
||||||
|
await expect(element(by.id('welcome'))).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show hello screen after tap', async () => {
|
||||||
|
await element(by.id('hello_button')).tap();
|
||||||
|
await expect(element(by.text('Hello!!!'))).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show world screen after tap', async () => {
|
||||||
|
await element(by.id('world_button')).tap();
|
||||||
|
await expect(element(by.text('World!!!'))).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
|
@ -4,7 +4,7 @@ import { ensureWorkerInit } from '@/src/lib/worker-init'
|
||||||
|
|
||||||
const LAST_TRIGGER_KEY = 'lastTrigger'
|
const LAST_TRIGGER_KEY = 'lastTrigger'
|
||||||
const ALARM_DELAY_MINUTES = 0.5
|
const ALARM_DELAY_MINUTES = 0.5
|
||||||
// const ALARM_DELAY_MINUTES = 3
|
// const ALARM_DELAY_MINUTES = 0.1
|
||||||
|
|
||||||
export class CacheCreator {
|
export class CacheCreator {
|
||||||
private state: any
|
private state: any
|
||||||
|
|
@ -53,13 +53,16 @@ export class CacheCreator {
|
||||||
|
|
||||||
async alarm(): Promise<void> {
|
async alarm(): Promise<void> {
|
||||||
ensureWorkerInit(this.env)
|
ensureWorkerInit(this.env)
|
||||||
|
console.log('from the shceduler')
|
||||||
const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY)
|
const lastTrigger = await this.state.storage.get(LAST_TRIGGER_KEY)
|
||||||
if (!lastTrigger) {
|
if (!lastTrigger) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const threshold = dayjs().subtract(ALARM_DELAY_MINUTES, 'minute')
|
const threshold = dayjs().subtract(ALARM_DELAY_MINUTES, 'minute')
|
||||||
if (dayjs(lastTrigger).isBefore(threshold)) {
|
const isQualify = dayjs(lastTrigger).isBefore(threshold);
|
||||||
|
console.log({isQualify, threshold, curr: dayjs(lastTrigger)})
|
||||||
|
if (isQualify) {
|
||||||
await initializeAllStores()
|
await initializeAllStores()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,15 @@ async function createProductsFileInternal(version: number): Promise<string> {
|
||||||
const productsData = await scaffoldProducts()
|
const productsData = await scaffoldProducts()
|
||||||
const jsonContent = JSON.stringify(productsData, null, 2)
|
const jsonContent = JSON.stringify(productsData, null, 2)
|
||||||
const buffer = Buffer.from(jsonContent, 'utf-8')
|
const buffer = Buffer.from(jsonContent, 'utf-8')
|
||||||
|
const filePath = `${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
|
||||||
|
|
||||||
|
console.log(filePath)
|
||||||
return await imageUploadS3(
|
return await imageUploadS3(
|
||||||
buffer,
|
buffer,
|
||||||
'application/json',
|
'application/json',
|
||||||
`${getApiCacheKey()}/${buildCachePath(CACHE_FILENAMES.products, version)}`
|
filePath
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createEssentialConstsFileInternal(version: number): Promise<string> {
|
async function createEssentialConstsFileInternal(version: number): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,79 @@
|
||||||
import fs from "fs";
|
// import fs from "fs";
|
||||||
import path from "path";
|
// import path from "path";
|
||||||
|
//
|
||||||
export class DiskPersistedSet {
|
// export class DiskPersistedSet {
|
||||||
private set: Set<string>;
|
// private set: Set<string>;
|
||||||
private readonly filePath: string;
|
// private readonly filePath: string;
|
||||||
private dirty = false;
|
// private dirty = false;
|
||||||
|
//
|
||||||
constructor(filePath: string = "./persister") {
|
// constructor(filePath: string = "./persister") {
|
||||||
this.filePath = path.resolve(filePath);
|
// this.filePath = path.resolve(filePath);
|
||||||
|
//
|
||||||
// ✅ Ensure file exists
|
// // ✅ Ensure file exists
|
||||||
if (!fs.existsSync(this.filePath)) {
|
// if (!fs.existsSync(this.filePath)) {
|
||||||
fs.writeFileSync(this.filePath, "", "utf8");
|
// fs.writeFileSync(this.filePath, "", "utf8");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// ✅ Load existing values from file
|
// // ✅ Load existing values from file
|
||||||
const contents = fs.readFileSync(this.filePath, "utf8");
|
// const contents = fs.readFileSync(this.filePath, "utf8");
|
||||||
this.set = new Set(
|
// this.set = new Set(
|
||||||
contents.split("\n").map(x => x.trim()).filter(x => x.length > 0)
|
// contents.split("\n").map(x => x.trim()).filter(x => x.length > 0)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
this.registerExitHandlers();
|
// this.registerExitHandlers();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private persist() {
|
// private persist() {
|
||||||
if (!this.dirty) return;
|
// if (!this.dirty) return;
|
||||||
fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8");
|
// fs.writeFileSync(this.filePath, Array.from(this.set).join("\n"), "utf8");
|
||||||
this.dirty = false;
|
// this.dirty = false;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private markDirty() {
|
// private markDirty() {
|
||||||
this.dirty = true;
|
// this.dirty = true;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
add(value: string): void {
|
// add(value: string): void {
|
||||||
if (!this.set.has(value)) {
|
// if (!this.set.has(value)) {
|
||||||
this.set.add(value);
|
// this.set.add(value);
|
||||||
this.markDirty();
|
// this.markDirty();
|
||||||
this.persist();
|
// this.persist();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
delete(value: string): void {
|
// delete(value: string): void {
|
||||||
if (this.set.delete(value)) {
|
// if (this.set.delete(value)) {
|
||||||
this.markDirty();
|
// this.markDirty();
|
||||||
this.persist();
|
// this.persist();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
has(value: string): boolean {
|
// has(value: string): boolean {
|
||||||
return this.set.has(value);
|
// return this.set.has(value);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
values(): string[] {
|
// values(): string[] {
|
||||||
return Array.from(this.set);
|
// return Array.from(this.set);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
clear(): void {
|
// clear(): void {
|
||||||
if (this.set.size > 0) {
|
// if (this.set.size > 0) {
|
||||||
this.set.clear();
|
// this.set.clear();
|
||||||
this.markDirty();
|
// this.markDirty();
|
||||||
this.persist();
|
// this.persist();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private registerExitHandlers() {
|
// private registerExitHandlers() {
|
||||||
const flush = () => this.persist();
|
// const flush = () => this.persist();
|
||||||
|
//
|
||||||
process.on("exit", flush);
|
// process.on("exit", flush);
|
||||||
process.on("SIGINT", () => { flush(); process.exit(); });
|
// process.on("SIGINT", () => { flush(); process.exit(); });
|
||||||
process.on("SIGTERM", () => { flush(); process.exit(); });
|
// process.on("SIGTERM", () => { flush(); process.exit(); });
|
||||||
process.on("uncaughtException", (err) => {
|
// process.on("uncaughtException", (err) => {
|
||||||
console.error("Uncaught exception. Flushing DiskPersistedSet:", err);
|
// console.error("Uncaught exception. Flushing DiskPersistedSet:", err);
|
||||||
flush();
|
// flush();
|
||||||
process.exit(1);
|
// process.exit(1);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
193
apps/backend/src/lib/s3-client.ts
Executable file → Normal file
193
apps/backend/src/lib/s3-client.ts
Executable file → Normal file
|
|
@ -1,8 +1,5 @@
|
||||||
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
|
|
||||||
import type { Buffer } from 'buffer'
|
import type { Buffer } from 'buffer'
|
||||||
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
import { AwsClient } from 'aws4fetch'
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
|
||||||
// import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility
|
|
||||||
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
|
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
|
||||||
import {
|
import {
|
||||||
getS3AccessKeyId,
|
getS3AccessKeyId,
|
||||||
|
|
@ -11,74 +8,81 @@ import {
|
||||||
getS3SecretAccessKey,
|
getS3SecretAccessKey,
|
||||||
getS3BucketName,
|
getS3BucketName,
|
||||||
getAssetsDomain,
|
getAssetsDomain,
|
||||||
} from "@/src/lib/env-exporter"
|
} from '@/src/lib/env-exporter'
|
||||||
|
|
||||||
let s3Client: S3Client | null = null
|
let awsClient: AwsClient | null = null
|
||||||
let s3ClientKey = ''
|
let awsClientKey = ''
|
||||||
|
|
||||||
const getS3Client = () => {
|
const getAwsClient = () => {
|
||||||
const region = getS3Region()
|
const region = getS3Region()
|
||||||
const endpoint = getS3Url()
|
const endpoint = getS3Url()
|
||||||
const accessKeyId = getS3AccessKeyId()
|
const accessKeyId = getS3AccessKeyId()
|
||||||
const secretAccessKey = getS3SecretAccessKey()
|
const secretAccessKey = getS3SecretAccessKey()
|
||||||
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
|
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
|
||||||
|
|
||||||
if (!s3Client || nextKey !== s3ClientKey) {
|
if (!awsClient || nextKey !== awsClientKey) {
|
||||||
s3ClientKey = nextKey
|
awsClientKey = nextKey
|
||||||
s3Client = new S3Client({
|
awsClient = new AwsClient({
|
||||||
|
accessKeyId,
|
||||||
|
secretAccessKey,
|
||||||
region,
|
region,
|
||||||
endpoint,
|
service: 's3',
|
||||||
forcePathStyle: true,
|
|
||||||
credentials: {
|
|
||||||
accessKeyId,
|
|
||||||
secretAccessKey,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return s3Client
|
return awsClient
|
||||||
}
|
}
|
||||||
|
|
||||||
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
|
const buildObjectUrl = (bucket: string, key: string) => {
|
||||||
// const key = `${category}/${Date.now()}`
|
const endpoint = getS3Url()
|
||||||
const s3BucketName = getS3BucketName()
|
const normalizedEndpoint = endpoint.endsWith('/')
|
||||||
const s3Client = getS3Client()
|
? endpoint.slice(0, -1)
|
||||||
const command = new PutObjectCommand({
|
: endpoint
|
||||||
Bucket: s3BucketName,
|
const normalizedKey = key.replace(/^\/+/, '')
|
||||||
Key: key,
|
return `${normalizedEndpoint}/${bucket}/${normalizedKey}`
|
||||||
Body: body,
|
}
|
||||||
ContentType: type,
|
|
||||||
|
export const imageUploadS3 = async(body: Buffer, type: string, key: string) => {
|
||||||
|
const client = getAwsClient()
|
||||||
|
const url = buildObjectUrl(getS3BucketName(), key)
|
||||||
|
const resp = await client.fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': type,
|
||||||
|
},
|
||||||
|
body,
|
||||||
})
|
})
|
||||||
const resp = await s3Client.send(command)
|
if (!resp.ok) {
|
||||||
|
const responseBody = await resp.text().catch(() => '')
|
||||||
|
throw new Error(`Failed to upload image: ${resp.status} ${responseBody}`)
|
||||||
|
}
|
||||||
const imageUrl = `${key}`
|
const imageUrl = `${key}`
|
||||||
return imageUrl;
|
return imageUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
|
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
|
||||||
|
|
||||||
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?:string, keys: string[]}) {
|
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?: string, keys: string[]}) {
|
||||||
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const s3Client = getS3Client()
|
const client = getAwsClient()
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
keys.map((key) => {
|
keys.map(async (key) => {
|
||||||
const deleteCommand = new DeleteObjectCommand({
|
const url = buildObjectUrl(bucket, key)
|
||||||
Bucket: bucket,
|
const resp = await client.fetch(url, { method: 'DELETE' })
|
||||||
Key: key,
|
if (!resp.ok && resp.status !== 404) {
|
||||||
})
|
const body = await resp.text().catch(() => '')
|
||||||
return s3Client.send(deleteCommand)
|
throw new Error(`Failed to delete image: ${resp.status} ${body}`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error("Error deleting image:", error)
|
console.error('Error deleting image:', error)
|
||||||
throw new Error("Failed to delete image")
|
throw new Error('Failed to delete image')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,19 +91,18 @@ export function scaffoldAssetUrl(input: (string | null)[]): string[]
|
||||||
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
|
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
|
||||||
const assetsDomain = getAssetsDomain()
|
const assetsDomain = getAssetsDomain()
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
return input.map(key => scaffoldAssetUrl(key) as string);
|
return input.map(key => scaffoldAssetUrl(key) as string)
|
||||||
}
|
}
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return '';
|
return ''
|
||||||
}
|
}
|
||||||
const normalizedKey = input.replace(/^\/+/, '');
|
const normalizedKey = input.replace(/^\/+/, '')
|
||||||
const domain = assetsDomain.endsWith('/')
|
const domain = assetsDomain.endsWith('/')
|
||||||
? assetsDomain.slice(0, -1)
|
? assetsDomain.slice(0, -1)
|
||||||
: assetsDomain;
|
: assetsDomain
|
||||||
return `${domain}/${normalizedKey}`;
|
return `${domain}/${normalizedKey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a signed URL from an S3 URL
|
* Generate a signed URL from an S3 URL
|
||||||
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
|
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
|
||||||
|
|
@ -108,34 +111,23 @@ export function scaffoldAssetUrl(input: string | null | (string | null)[]): stri
|
||||||
*/
|
*/
|
||||||
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
|
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
|
||||||
if (!s3UrlRaw) {
|
if (!s3UrlRaw) {
|
||||||
return '';
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const s3Url = s3UrlRaw
|
const s3Url = s3UrlRaw
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Cache disabled for Workers compatibility
|
const client = getAwsClient()
|
||||||
// const cachedUrl = signedUrlCache.get(s3Url);
|
const url = buildObjectUrl(getS3BucketName(), s3Url)
|
||||||
// if (cachedUrl) {
|
const signedRequest = await client.sign(url, {
|
||||||
// return cachedUrl;
|
method: 'GET',
|
||||||
// }
|
signQuery: true,
|
||||||
|
expires: expiresIn,
|
||||||
// Create the command to get the object
|
})
|
||||||
const command = new GetObjectCommand({
|
return signedRequest.url
|
||||||
Bucket: getS3BucketName(),
|
|
||||||
Key: s3Url,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate the signed URL
|
|
||||||
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
|
|
||||||
|
|
||||||
// Cache disabled for Workers compatibility
|
|
||||||
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
|
|
||||||
|
|
||||||
return signedUrl;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating signed URL:", error);
|
console.error('Error generating signed URL:', error)
|
||||||
throw new Error("Failed to generate signed URL");
|
throw new Error('Failed to generate signed URL')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +139,7 @@ export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresI
|
||||||
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
|
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
|
||||||
// Cache disabled for Workers compatibility - cannot retrieve original URL without cache
|
// Cache disabled for Workers compatibility - cannot retrieve original URL without cache
|
||||||
// To re-enable, migrate signed-url-cache to object storage (R2/S3)
|
// To re-enable, migrate signed-url-cache to object storage (R2/S3)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -158,44 +150,43 @@ export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null
|
||||||
*/
|
*/
|
||||||
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
|
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
|
||||||
if (!s3Urls || !s3Urls.length) {
|
if (!s3Urls || !s3Urls.length) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process URLs in parallel for better performance
|
|
||||||
const signedUrls = await Promise.all(
|
const signedUrls = await Promise.all(
|
||||||
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
|
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
|
||||||
);
|
)
|
||||||
|
|
||||||
return signedUrls;
|
return signedUrls
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating multiple signed URLs:", error);
|
console.error('Error generating multiple signed URLs:', error)
|
||||||
// Return an array of empty strings with the same length as input
|
return s3Urls.map(() => '')
|
||||||
return s3Urls.map(() => '');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
|
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// Insert record into upload_url_status
|
|
||||||
await createUploadUrlStatus(key)
|
await createUploadUrlStatus(key)
|
||||||
|
|
||||||
// Generate signed upload URL
|
const client = getAwsClient()
|
||||||
const command = new PutObjectCommand({
|
const url = buildObjectUrl(getS3BucketName(), key)
|
||||||
Bucket: getS3BucketName(),
|
const signedRequest = await client.sign(url, {
|
||||||
Key: key,
|
method: 'PUT',
|
||||||
ContentType: mimeType,
|
signQuery: true,
|
||||||
});
|
expires: expiresIn,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': mimeType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
|
return signedRequest.url
|
||||||
return signedUrl;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating upload URL:', error);
|
console.error('Error generating upload URL:', error)
|
||||||
throw new Error('Failed to generate upload URL');
|
throw new Error('Failed to generate upload URL')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// export function extractKeyFromPresignedUrl(url:string) {
|
// export function extractKeyFromPresignedUrl(url:string) {
|
||||||
// const u = new URL(url);
|
// const u = new URL(url);
|
||||||
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
|
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
|
||||||
|
|
@ -204,27 +195,27 @@ export async function generateUploadUrl(key: string, mimeType: string, expiresIn
|
||||||
|
|
||||||
// New function (excludes bucket name)
|
// New function (excludes bucket name)
|
||||||
export function extractKeyFromPresignedUrl(url: string): string {
|
export function extractKeyFromPresignedUrl(url: string): string {
|
||||||
const u = new URL(url);
|
const u = new URL(url)
|
||||||
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
|
const rawKey = u.pathname.replace(/^\/+/, '') // remove leading slash
|
||||||
const decodedKey = decodeURIComponent(rawKey);
|
const decodedKey = decodeURIComponent(rawKey)
|
||||||
// Remove bucket prefix
|
// Remove bucket prefix
|
||||||
const parts = decodedKey.split('/');
|
const parts = decodedKey.split('/')
|
||||||
parts.shift(); // Remove bucket name
|
parts.shift() // Remove bucket name
|
||||||
return parts.join('/');
|
return parts.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function claimUploadUrl(url: string): Promise<void> {
|
export async function claimUploadUrl(url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const semiKey = extractKeyFromPresignedUrl(url);
|
const semiKey = extractKeyFromPresignedUrl(url)
|
||||||
|
|
||||||
// Update status to 'claimed' if currently 'pending'
|
// Update status to 'claimed' if currently 'pending'
|
||||||
const updated = await claimUploadUrlStatus(semiKey)
|
const updated = await claimUploadUrlStatus(semiKey)
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
throw new Error('Upload URL not found or already claimed');
|
throw new Error('Upload URL not found or already claimed')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error claiming upload URL:', error);
|
console.error('Error claiming upload URL:', error)
|
||||||
throw new Error('Failed to claim upload URL');
|
throw new Error('Failed to claim upload URL')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
244
apps/backend/src/lib/s3-client.ts.txt
Normal file
244
apps/backend/src/lib/s3-client.ts.txt
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
|
||||||
|
import type { Buffer } from 'buffer'
|
||||||
|
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
|
||||||
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||||
|
// import signedUrlCache from "@/src/lib/signed-url-cache" // Disabled for Workers compatibility
|
||||||
|
import { claimUploadUrlStatus, createUploadUrlStatus } from '@/src/dbService'
|
||||||
|
import {
|
||||||
|
getS3AccessKeyId,
|
||||||
|
getS3Region,
|
||||||
|
getS3Url,
|
||||||
|
getS3SecretAccessKey,
|
||||||
|
getS3BucketName,
|
||||||
|
getAssetsDomain,
|
||||||
|
} from "@/src/lib/env-exporter"
|
||||||
|
|
||||||
|
let s3Client: S3Client | null = null
|
||||||
|
let s3ClientKey = ''
|
||||||
|
|
||||||
|
const getS3Client = () => {
|
||||||
|
const region = getS3Region()
|
||||||
|
const endpoint = getS3Url()
|
||||||
|
const accessKeyId = getS3AccessKeyId()
|
||||||
|
const secretAccessKey = getS3SecretAccessKey()
|
||||||
|
const nextKey = `${region}|${endpoint}|${accessKeyId}|${secretAccessKey}`
|
||||||
|
|
||||||
|
if (!s3Client || nextKey !== s3ClientKey) {
|
||||||
|
s3ClientKey = nextKey
|
||||||
|
s3Client = new S3Client({
|
||||||
|
region,
|
||||||
|
endpoint,
|
||||||
|
forcePathStyle: true,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId,
|
||||||
|
secretAccessKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return s3Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
|
||||||
|
// // const key = `${category}/${Date.now()}`
|
||||||
|
// const s3BucketName = getS3BucketName()
|
||||||
|
// const s3Client = getS3Client()
|
||||||
|
// const command = new PutObjectCommand({
|
||||||
|
// Bucket: s3BucketName,
|
||||||
|
// Key: key,
|
||||||
|
// Body: body,
|
||||||
|
// ContentType: type,
|
||||||
|
// })
|
||||||
|
// const resp = await s3Client.send(command)
|
||||||
|
//
|
||||||
|
// const imageUrl = `${key}`
|
||||||
|
// return imageUrl;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const imageUploadS3 = async(body: Buffer, type: string, key:string) => {
|
||||||
|
const env = (globalThis as any).ENV || {}
|
||||||
|
if (!env.MY_BUCKET) {
|
||||||
|
throw new Error('MY_BUCKET binding not found in runtime env')
|
||||||
|
}
|
||||||
|
await env.MY_BUCKET.put(key, body, {
|
||||||
|
httpMetadata: {
|
||||||
|
contentType: type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const imageUrl = `${key}`
|
||||||
|
return imageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
|
||||||
|
|
||||||
|
export async function deleteImageUtil({bucket = getS3BucketName(), keys}:{bucket?:string, keys: string[]}) {
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const s3Client = getS3Client()
|
||||||
|
await Promise.all(
|
||||||
|
keys.map((key) => {
|
||||||
|
const deleteCommand = new DeleteObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key,
|
||||||
|
})
|
||||||
|
return s3Client.send(deleteCommand)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Error deleting image:", error)
|
||||||
|
throw new Error("Failed to delete image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scaffoldAssetUrl(input: string | null): string
|
||||||
|
export function scaffoldAssetUrl(input: (string | null)[]): string[]
|
||||||
|
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
|
||||||
|
const assetsDomain = getAssetsDomain()
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input.map(key => scaffoldAssetUrl(key) as string);
|
||||||
|
}
|
||||||
|
if (!input) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const normalizedKey = input.replace(/^\/+/, '');
|
||||||
|
const domain = assetsDomain.endsWith('/')
|
||||||
|
? assetsDomain.slice(0, -1)
|
||||||
|
: assetsDomain;
|
||||||
|
return `${domain}/${normalizedKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a signed URL from an S3 URL
|
||||||
|
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
|
||||||
|
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
|
||||||
|
* @returns A pre-signed URL that provides temporary access to the object
|
||||||
|
*/
|
||||||
|
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
|
||||||
|
if (!s3UrlRaw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3Url = s3UrlRaw
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cache disabled for Workers compatibility
|
||||||
|
// const cachedUrl = signedUrlCache.get(s3Url);
|
||||||
|
// if (cachedUrl) {
|
||||||
|
// return cachedUrl;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Create the command to get the object
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: getS3BucketName(),
|
||||||
|
Key: s3Url,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate the signed URL
|
||||||
|
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
|
||||||
|
|
||||||
|
// Cache disabled for Workers compatibility
|
||||||
|
// signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000);
|
||||||
|
|
||||||
|
return signedUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating signed URL:", error);
|
||||||
|
throw new Error("Failed to generate signed URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the original S3 URL from a signed URL
|
||||||
|
* @param signedUrl The signed URL
|
||||||
|
* @returns The original S3 URL if found in cache, otherwise null
|
||||||
|
*/
|
||||||
|
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
|
||||||
|
// Cache disabled for Workers compatibility - cannot retrieve original URL without cache
|
||||||
|
// To re-enable, migrate signed-url-cache to object storage (R2/S3)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate signed URLs for multiple S3 URLs
|
||||||
|
* @param s3Urls Array of S3 URLs or null values
|
||||||
|
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
|
||||||
|
* @returns Array of signed URLs (empty strings for null/invalid inputs)
|
||||||
|
*/
|
||||||
|
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
|
||||||
|
if (!s3Urls || !s3Urls.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process URLs in parallel for better performance
|
||||||
|
const signedUrls = await Promise.all(
|
||||||
|
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
|
||||||
|
);
|
||||||
|
|
||||||
|
return signedUrls;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating multiple signed URLs:", error);
|
||||||
|
// Return an array of empty strings with the same length as input
|
||||||
|
return s3Urls.map(() => '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Insert record into upload_url_status
|
||||||
|
await createUploadUrlStatus(key)
|
||||||
|
|
||||||
|
// Generate signed upload URL
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: getS3BucketName(),
|
||||||
|
Key: key,
|
||||||
|
ContentType: mimeType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const signedUrl = await getSignedUrl(getS3Client(), command, { expiresIn });
|
||||||
|
return signedUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating upload URL:', error);
|
||||||
|
throw new Error('Failed to generate upload URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// export function extractKeyFromPresignedUrl(url:string) {
|
||||||
|
// const u = new URL(url);
|
||||||
|
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
|
||||||
|
// return decodeURIComponent(rawKey);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// New function (excludes bucket name)
|
||||||
|
export function extractKeyFromPresignedUrl(url: string): string {
|
||||||
|
const u = new URL(url);
|
||||||
|
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
|
||||||
|
const decodedKey = decodeURIComponent(rawKey);
|
||||||
|
// Remove bucket prefix
|
||||||
|
const parts = decodedKey.split('/');
|
||||||
|
parts.shift(); // Remove bucket name
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function claimUploadUrl(url: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const semiKey = extractKeyFromPresignedUrl(url);
|
||||||
|
|
||||||
|
// Update status to 'claimed' if currently 'pending'
|
||||||
|
const updated = await claimUploadUrlStatus(semiKey)
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error('Upload URL not found or already claimed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error claiming upload URL:', error);
|
||||||
|
throw new Error('Failed to claim upload URL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -226,7 +226,7 @@ export const productRouter = router({
|
||||||
price: z.number().positive('Price must be positive'),
|
price: z.number().positive('Price must be positive'),
|
||||||
marketPrice: z.number().optional(),
|
marketPrice: z.number().optional(),
|
||||||
incrementStep: z.number().optional().default(1),
|
incrementStep: z.number().optional().default(1),
|
||||||
productQuantity: z.number().optional().default(1),
|
productQuantity: z.union([z.number(), z.string()]).optional().default(1),
|
||||||
isSuspended: z.boolean().optional().default(false),
|
isSuspended: z.boolean().optional().default(false),
|
||||||
isFlashAvailable: z.boolean().optional().default(false),
|
isFlashAvailable: z.boolean().optional().default(false),
|
||||||
flashPrice: z.number().optional(),
|
flashPrice: z.number().optional(),
|
||||||
|
|
@ -262,7 +262,7 @@ export const productRouter = router({
|
||||||
price: price.toString(),
|
price: price.toString(),
|
||||||
marketPrice: marketPrice?.toString(),
|
marketPrice: marketPrice?.toString(),
|
||||||
incrementStep,
|
incrementStep,
|
||||||
productQuantity,
|
productQuantity: productQuantity as any,
|
||||||
isSuspended,
|
isSuspended,
|
||||||
isFlashAvailable,
|
isFlashAvailable,
|
||||||
flashPrice: flashPrice?.toString(),
|
flashPrice: flashPrice?.toString(),
|
||||||
|
|
@ -302,7 +302,7 @@ export const productRouter = router({
|
||||||
price: z.number().positive('Price must be positive'),
|
price: z.number().positive('Price must be positive'),
|
||||||
marketPrice: z.number().optional(),
|
marketPrice: z.number().optional(),
|
||||||
incrementStep: z.number().optional().default(1),
|
incrementStep: z.number().optional().default(1),
|
||||||
productQuantity: z.number().optional().default(1),
|
productQuantity: z.union([z.number(), z.string()]).optional().default(1),
|
||||||
isSuspended: z.boolean().optional().default(false),
|
isSuspended: z.boolean().optional().default(false),
|
||||||
isFlashAvailable: z.boolean().optional().default(false),
|
isFlashAvailable: z.boolean().optional().default(false),
|
||||||
flashPrice: z.number().nullable().optional(),
|
flashPrice: z.number().nullable().optional(),
|
||||||
|
|
@ -347,7 +347,7 @@ export const productRouter = router({
|
||||||
price: price.toString(),
|
price: price.toString(),
|
||||||
marketPrice: marketPrice?.toString(),
|
marketPrice: marketPrice?.toString(),
|
||||||
incrementStep,
|
incrementStep,
|
||||||
productQuantity,
|
productQuantity: productQuantity as any,
|
||||||
isSuspended,
|
isSuspended,
|
||||||
isFlashAvailable,
|
isFlashAvailable,
|
||||||
flashPrice: flashPrice?.toString() ?? null,
|
flashPrice: flashPrice?.toString() ?? null,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ queue = "order-placed-queue-dev"
|
||||||
[[queues.consumers]]
|
[[queues.consumers]]
|
||||||
queue = "order-cancelled-queue-dev"
|
queue = "order-cancelled-queue-dev"
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = "MY_BUCKET"
|
||||||
|
bucket_name = "meatfarmer-dev"
|
||||||
|
|
||||||
[observability]
|
[observability]
|
||||||
enabled = false
|
enabled = false
|
||||||
head_sampling_rate = 1
|
head_sampling_rate = 1
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ queue = "order-placed-queue"
|
||||||
[[queues.consumers]]
|
[[queues.consumers]]
|
||||||
queue = "order-cancelled-queue"
|
queue = "order-cancelled-queue"
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = "MY_BUCKET"
|
||||||
|
bucket_name = "meatfarmer"
|
||||||
|
|
||||||
[observability]
|
[observability]
|
||||||
enabled = false
|
enabled = false
|
||||||
head_sampling_rate = 1
|
head_sampling_rate = 1
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import AddressForm from '@/src/components/AddressForm';
|
||||||
import LocationAttacher from '@/src/components/LocationAttacher';
|
import LocationAttacher from '@/src/components/LocationAttacher';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
import { useAuth } from '@/src/contexts/AuthContext';
|
||||||
import * as Location from 'expo-location';
|
import * as Location from 'expo-location';
|
||||||
|
|
||||||
interface AddressSelectorProps {
|
interface AddressSelectorProps {
|
||||||
|
|
@ -22,7 +23,11 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
|
||||||
const [locationLoading, setLocationLoading] = useState(false);
|
const [locationLoading, setLocationLoading] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery();
|
const { isAuthenticated } = useAuth();
|
||||||
|
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery(
|
||||||
|
undefined,
|
||||||
|
{ enabled: isAuthenticated }
|
||||||
|
);
|
||||||
|
|
||||||
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({
|
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
return desc;
|
return desc;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery();
|
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery(
|
||||||
|
undefined,
|
||||||
|
{ enabled: isAuthenticated }
|
||||||
|
);
|
||||||
const { data: constsData } = useGetEssentialConsts();
|
const { data: constsData } = useGetEssentialConsts();
|
||||||
const products = useCentralProductStore((state) => state.products);
|
const products = useCentralProductStore((state) => state.products);
|
||||||
const productsById = useCentralProductStore((state) => state.productsById);
|
const productsById = useCentralProductStore((state) => state.productsById);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import AddressForm from '@/src/components/AddressForm';
|
import AddressForm from '@/src/components/AddressForm';
|
||||||
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
|
import { useAuthenticatedRoute } from '@/hooks/useAuthenticatedRoute';
|
||||||
|
import { useAuth } from '@/src/contexts/AuthContext';
|
||||||
|
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
import { useCentralProductStore } from '@/src/store/centralProductStore';
|
||||||
|
|
@ -24,6 +25,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isAuthenticated } = useAuth();
|
||||||
|
|
||||||
// Protect checkout route and preserve query params
|
// Protect checkout route and preserve query params
|
||||||
useAuthenticatedRoute({
|
useAuthenticatedRoute({
|
||||||
|
|
@ -34,7 +36,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular";
|
const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular";
|
||||||
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
|
const { data: cartData, refetch: refetchCart } = useGetCart({}, cartType);
|
||||||
|
|
||||||
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery();
|
const { data: addresses, refetch: refetchAddresses } = trpc.user.address.getUserAddresses.useQuery(
|
||||||
|
undefined,
|
||||||
|
{ enabled: isAuthenticated }
|
||||||
|
);
|
||||||
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
|
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlots.useQuery();
|
||||||
const { data: constsData } = useGetEssentialConsts();
|
const { data: constsData } = useGetEssentialConsts();
|
||||||
const products = useCentralProductStore((state) => state.products);
|
const products = useCentralProductStore((state) => state.products);
|
||||||
|
|
@ -137,7 +142,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery();
|
const { data: couponsRaw } = trpc.user.coupon.getEligible.useQuery(
|
||||||
|
undefined,
|
||||||
|
{ enabled: isAuthenticated }
|
||||||
|
);
|
||||||
|
|
||||||
const eligibleCoupons = useMemo(() => {
|
const eligibleCoupons = useMemo(() => {
|
||||||
if (!couponsRaw?.data) return [];
|
if (!couponsRaw?.data) return [];
|
||||||
|
|
|
||||||
|
|
@ -163,13 +163,38 @@ type ProductInfoInsert = InferInsertModel<typeof productInfo>
|
||||||
type ProductInfoUpdate = Partial<ProductInfoInsert>
|
type ProductInfoUpdate = Partial<ProductInfoInsert>
|
||||||
|
|
||||||
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
|
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
|
||||||
const [product] = await db.insert(productInfo).values(input).returning()
|
const productQuantityRaw = (input as any).productQuantity
|
||||||
|
const productQuantity = typeof productQuantityRaw === 'string'
|
||||||
|
? Number(productQuantityRaw)
|
||||||
|
: productQuantityRaw
|
||||||
|
|
||||||
|
const safeProductQuantity = typeof productQuantity === 'number' && Number.isFinite(productQuantity)
|
||||||
|
? productQuantity
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const [product] = await db.insert(productInfo).values({
|
||||||
|
...input,
|
||||||
|
productQuantity: safeProductQuantity,
|
||||||
|
}).returning()
|
||||||
return mapProduct(product)
|
return mapProduct(product)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
|
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
|
||||||
|
const productQuantityRaw = (updates as any).productQuantity
|
||||||
|
const productQuantity = typeof productQuantityRaw === 'string'
|
||||||
|
? Number(productQuantityRaw)
|
||||||
|
: productQuantityRaw
|
||||||
|
const safeUpdates = typeof productQuantityRaw === 'undefined'
|
||||||
|
? updates
|
||||||
|
: {
|
||||||
|
...updates,
|
||||||
|
productQuantity: typeof productQuantity === 'number' && Number.isFinite(productQuantity)
|
||||||
|
? productQuantity
|
||||||
|
: 1,
|
||||||
|
}
|
||||||
|
|
||||||
const [product] = await db.update(productInfo)
|
const [product] = await db.update(productInfo)
|
||||||
.set(updates)
|
.set(safeUpdates)
|
||||||
.where(eq(productInfo.id, id))
|
.where(eq(productInfo.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
if (!product) {
|
if (!product) {
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
||||||
// const BASE_API_URL = API_URL;
|
// const BASE_API_URL = API_URL;
|
||||||
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.1.5:4000';
|
const BASE_API_URL = 'http://192.168.1.5:8787';
|
||||||
const BASE_API_URL = 'http://192.168.100.109:8787';
|
// const BASE_API_URL = 'http://192.168.100.109:8787';
|
||||||
// let BASE_API_URL = "https://raw.freshyo.in";
|
// let BASE_API_URL = "https://raw.freshyo.in";
|
||||||
// let BASE_API_URL = "https://worker.freshyo.in";
|
// let BASE_API_URL = "https://worker.freshyo.in";
|
||||||
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
// let BASE_API_URL = "https://freshyo.technocracy.ovh";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue