feat(kvot): debitera AI-skanning PER BILD + fixa dubbeldebitering

Johans beslut: en skanning debiteras per bild (6 foton = 6 skanningar), streckkod
gratis. consumeAiScan tar nu units=imageCount och kollar att hela ryms.
Refunden ger tillbaka lika manga (antal bilder) vid misslyckat jobb. Appen kapar
antalet foton till kvarvarande kvot och gar till paywall om 0 (servern ar dock
den slutgiltiga grinden).

Hittade + fixade en PRE-EXISTING bugg pa vagen: kvoten drogs bade vid skapande
(consumeAiScan) OCH vid slutford analys (recordAiUsage aiScans:1) -> varje LYCKAD
skanning kostade 2 enheter, dolt av att appen bara uppdaterade raknaren vid start.
Nu dras kvoten exakt en gang, vid skapandet. Enbildsanvandare far alltsa nu sina
fulla 5 gratis-skanningar (var tidigare ~2,5).
This commit is contained in:
Claude
2026-08-18 21:13:58 +00:00
parent 3bb1d309bc
commit 55b03b5039
5 changed files with 101 additions and 51 deletions
+29 -13
View File
@@ -44,17 +44,17 @@ const MULTI_PHOTO_TYPES = new Set(["fridge", "freezer", "pantry", "ingredients"]
const MAX_PHOTOS = 6; // matchar createScanInputSchema.imageCount.max
/** Fråga om användaren vill ta fler bilder i samma skanning. */
function askAddMore(count: number): Promise<boolean> {
function askAddMore(count: number, maxCount: number): Promise<boolean> {
return new Promise((resolve) => {
if (count >= MAX_PHOTOS) {
Alert.alert("Klart", `Max ${MAX_PHOTOS} bilder analyserar.`, [
if (count >= maxCount) {
Alert.alert("Klart", `Max ${maxCount} bilder den här skanningen analyserar.`, [
{ text: "OK", onPress: () => resolve(false) },
]);
return;
}
Alert.alert(
`Bild ${count} tagen`,
"Ta gärna fler vinklar av samma plats appen listar varje vara en gång.",
"Ta gärna fler vinklar av samma plats appen listar varje vara en gång. Varje bild räknas som en skanning.",
[
{ text: "Ta en till", onPress: () => resolve(true) },
{ text: `Analysera (${count})`, style: "default", onPress: () => resolve(false) },
@@ -63,13 +63,17 @@ function askAddMore(count: number): Promise<boolean> {
});
}
/** Fånga en eller flera bilder. Kamera → loop med "ta en till"; galleri → flerval. */
async function capturePhotos(multi: boolean): Promise<string[]> {
/**
* Fånga en eller flera bilder. Kamera → loop med "ta en till"; galleri → flerval.
* maxCount kapar antalet till kvarvarande kvot (varje bild = en skanning).
*/
async function capturePhotos(multi: boolean, maxCount: number): Promise<string[]> {
const permission = await ImagePicker.requestCameraPermissionsAsync();
const useCamera = permission.granted;
const uris: string[] = [];
const cap = Math.max(1, maxCount);
if (!multi) {
if (!multi || cap === 1) {
const picked = useCamera
? await ImagePicker.launchCameraAsync({ quality: 0.7 })
: await ImagePicker.launchImageLibraryAsync({ quality: 0.7 });
@@ -83,19 +87,19 @@ async function capturePhotos(multi: boolean): Promise<string[]> {
const picked = await ImagePicker.launchImageLibraryAsync({
quality: 0.7,
allowsMultipleSelection: true,
selectionLimit: MAX_PHOTOS,
selectionLimit: cap,
});
if (!picked.canceled) for (const a of picked.assets ?? []) uris.push(a.uri);
if (!picked.canceled) for (const a of (picked.assets ?? []).slice(0, cap)) uris.push(a.uri);
return uris;
}
// Kamera: ta en bild i taget, fråga om fler mellan varje.
while (uris.length < MAX_PHOTOS) {
// Kamera: ta en bild i taget, fråga om fler mellan varje (upp till kvot-taket).
while (uris.length < cap) {
const picked = await ImagePicker.launchCameraAsync({ quality: 0.7 });
const asset = picked.assets?.[0];
if (picked.canceled || !asset) break;
uris.push(asset.uri);
const more = await askAddMore(uris.length);
const more = await askAddMore(uris.length, cap);
if (!more) break;
}
return uris;
@@ -131,9 +135,21 @@ export default function ScanScreen() {
return;
}
try {
// Varje bild = en skanning. Kapa antalet foton till kvarvarande kvot, och
// skicka till paywall direkt om kvoten redan är slut (servern är dock den
// som slutgiltigt räknar appens tak är bara för att slippa blockering mitt i).
const remaining = entitlements.data
? entitlements.data.aiScansPerMonth - entitlements.data.aiScansUsedThisMonth
: null;
if (remaining != null && remaining <= 0) {
router.push("/paywall");
return;
}
const cap = remaining != null ? Math.min(MAX_PHOTOS, remaining) : MAX_PHOTOS;
// 1. Ta bild(er) flera vinklar tillåtna för kyl/frys/skafferi/ingredienser.
const multi = MULTI_PHOTO_TYPES.has(scanType);
const uris = await capturePhotos(multi);
const uris = await capturePhotos(multi, cap);
if (uris.length === 0) return; // avbruten
// Visa laddning först nu (kamera/dialoger är egna vyer).