Files
com.blitzwallet/context-store/giftContext.js
T
2026-05-16 09:50:49 -04:00

376 lines
11 KiB
JavaScript

import React, {
createContext,
useContext,
useReducer,
useEffect,
useMemo,
useRef,
useCallback,
} from 'react';
import { useAppStatus } from './appStatus';
import {
bulkDeleteGiftsLocal,
bulkSaveGiftsLocal,
deleteGiftLocal,
getAllLocalGifts,
saveGiftLocal,
updateGiftLocal,
} from '../app/functions/gift/giftsStorage';
import {
addGiftToDatabase,
bulkAddGiftsToDatabase,
bulkDeleteGiftsFromDatabase,
deleteGift,
handleGiftCheck,
reloadGiftsOnDomesday,
updateGiftInDatabase,
} from '../db';
import { useGlobalContextProvider } from './context';
import { getLocalStorageItem, setLocalStorageItem } from '../app/functions';
import { deriveKeyFromMnemonic } from '../app/functions/seed';
import { useKeysContext } from './keys';
import { randomBytes } from 'react-native-quick-crypto';
import { getPublicKey } from 'nostr-tools';
import { encriptMessage } from '../app/functions/messaging/encodingAndDecodingMessages';
import { createGiftUrl } from '../app/functions/gift/encodeDecodeSecret';
import { deriveSparkGiftMnemonic } from '../app/functions/gift/deriveGiftWallet';
import { GIFT_DERIVE_PATH_CUTOFF } from '../app/constants';
const initialState = {
gifts: {},
currentDerivedGiftIndex: 0,
userUuid: null,
};
function giftReducer(state, action) {
switch (action.type) {
case 'LOAD_LOCAL_GIFTS':
return {
...state,
gifts: action.payload.reduce((map, g) => {
map[g.uuid] = g;
return map;
}, {}),
};
case 'ADD_OR_UPDATE_GIFT':
return {
...state,
gifts: { ...state.gifts, [action.payload.uuid]: action.payload },
};
case 'BULK_ADD_GIFTS':
return {
...state,
gifts: {
...state.gifts,
...action.payload.reduce((map, g) => {
map[g.uuid] = g;
return map;
}, {}),
},
};
case 'BULK_DELETE_GIFTS': {
const remaining = { ...state.gifts };
action.payload.forEach(uuid => delete remaining[uuid]);
return { ...state, gifts: remaining };
}
default:
return state;
}
}
const GiftContext = createContext(null);
export function GiftProvider({ children }) {
const { accountMnemoinc } = useKeysContext();
const { masterInfoObject } = useGlobalContextProvider();
const { didGetToHomepage } = useAppStatus();
const [state, dispatch] = useReducer(giftReducer, initialState);
const isCheckingRefunds = useRef(null);
const updateGiftList = useCallback(async () => {
const updatedList = await getAllLocalGifts();
dispatch({ type: 'LOAD_LOCAL_GIFTS', payload: updatedList });
return updatedList;
}, []);
const saveGiftToCloud = useCallback(async giftObj => {
try {
const localObject = JSON.parse(JSON.stringify(giftObj));
const localResponse = await saveGiftLocal(localObject);
delete giftObj.claimURL;
const serverResponse = await addGiftToDatabase(giftObj);
if (!serverResponse || !localResponse)
throw new Error('Unable to save gift');
dispatch({ type: 'ADD_OR_UPDATE_GIFT', payload: localObject });
return true;
} catch (err) {
console.log('error saving gift to cloud');
return false;
}
}, []);
const bulkSaveGiftsToCloud = useCallback(async gifts => {
try {
const localObjects = gifts.map(g => JSON.parse(JSON.stringify(g)));
const [localResponse, serverResponse] = await Promise.all([
bulkSaveGiftsLocal(localObjects),
bulkAddGiftsToDatabase(gifts),
]);
if (!localResponse || !serverResponse)
throw new Error('Unable to bulk save gifts');
dispatch({ type: 'BULK_ADD_GIFTS', payload: localObjects });
return true;
} catch (err) {
console.log('error bulk saving gifts to cloud', err);
return false;
}
}, []);
const bulkDeleteGiftsFromCloudAndLocal = useCallback(async uuids => {
try {
const [localResponse, serverResponse] = await Promise.all([
bulkDeleteGiftsLocal(uuids),
bulkDeleteGiftsFromDatabase(uuids),
]);
if (!localResponse || !serverResponse)
throw new Error('Unable to bulk delete gifts');
dispatch({
type: 'BULK_DELETE_GIFTS',
payload: uuids,
});
return true;
} catch (err) {
console.log('error bulk deleting gifts from cloud and local', err);
return false;
}
}, []);
const deleteGiftFromCloudAndLocal = useCallback(
async UUID => {
try {
await deleteGiftLocal(UUID);
const response = await deleteGift(UUID);
if (!response) throw new Error('Unable to delete gift from remote ');
await updateGiftList();
return true;
} catch (err) {
console.log('error saving gift to cloud');
return false;
}
},
[updateGiftList],
);
const checkForRefunds = useCallback(async giftList => {
try {
if (isCheckingRefunds.current) return;
isCheckingRefunds.current = true;
const localGifts = await (giftList
? Promise.resolve(giftList)
: getAllLocalGifts());
const giftArray = Object.values(localGifts);
const now = Date.now();
const expiredGifts = giftArray.filter(item => {
return item.state === 'Unclaimed' && now >= item.expireTime;
});
console.log(expiredGifts, 'expired gifts');
if (expiredGifts.length === 0) {
console.log('No expired gifts to check');
return;
}
console.log(`Checking ${expiredGifts.length} expired gifts...`);
const checkPromises = expiredGifts.map(card =>
handleGiftCheck(card.uuid)
.then(response => ({ card, response }))
.catch(error => {
console.error(`Error checking gift ${card.uuid}:`, error);
return { card, response: null };
}),
);
const results = await Promise.all(checkPromises);
// Batch database updates
const updatePromises = results
.filter(({ response }) => response?.didWork)
.map(async ({ card, response }) => {
console.log(card, response);
try {
if (response.wasClaimed) {
await deleteGift(card.uuid);
}
await updateGiftLocal(card.uuid, {
state: response.wasClaimed ? 'Claimed' : 'Expired',
});
console.log(
`Updated gift ${card.uuid}:`,
response.wasClaimed ? 'Claimed' : 'Expired',
);
} catch (error) {
console.error(`Error updating gift ${card.uuid}:`, error);
}
});
await Promise.all(updatePromises);
await updateGiftList();
console.log(`Processed ${updatePromises.length} gift updates`);
} catch (err) {
console.log('error checking for gift refunds', err.message);
} finally {
isCheckingRefunds.current = false;
}
}, [updateGiftList]);
const handleGiftRestoreOnDomeseday = useCallback(async giftList => {
// If we have gifts that means we are not restoring and do not need to get gifts in database
if (giftList?.length) return;
const didCheckDBForGifts = JSON.parse(
await getLocalStorageItem('checkForOutstandingGifts'),
);
// We already checked for outstanding gifts and none exist so don't check again
if (didCheckDBForGifts) return;
const outstandingGifts = await reloadGiftsOnDomesday(masterInfoObject.uuid);
// If no gifts exist, mark as checked and return
if (!outstandingGifts.length) {
await setLocalStorageItem(
'checkForOutstandingGifts',
JSON.stringify(true),
);
return;
}
const now = Date.now();
// Process all gifts in parallel and wait for completion
const reconstructedGifts = await Promise.all(
outstandingGifts.map(async item => {
let giftWalletMnemonic;
if (item.createdTime > GIFT_DERIVE_PATH_CUTOFF) {
giftWalletMnemonic = await deriveSparkGiftMnemonic(
accountMnemoinc,
item.giftNum,
);
} else {
giftWalletMnemonic = await deriveKeyFromMnemonic(
accountMnemoinc,
item.giftNum,
);
}
// Gift is expired - just add restore key
if (item.expireTime < now) {
const expiredGift = {
...item,
restoreKey: giftWalletMnemonic.derivedMnemonic,
};
await saveGiftLocal(expiredGift);
return expiredGift;
} else {
// Active gift - update with new secret for sharing
const randomSecret = randomBytes(32);
const randomPubkey = getPublicKey(randomSecret);
const encryptedMnemonic = encriptMessage(
randomSecret,
randomPubkey,
giftWalletMnemonic.derivedMnemonic,
);
const urls = createGiftUrl(item.uuid, randomSecret);
const updatedGift = {
...item,
claimURL: urls.webUrl,
encryptedText: encryptedMnemonic,
};
await Promise.all([
updateGiftInDatabase(updatedGift),
saveGiftLocal(updatedGift),
]);
return updatedGift;
}
}),
);
dispatch({ type: 'BULK_ADD_GIFTS', payload: reconstructedGifts });
await setLocalStorageItem('checkForOutstandingGifts', JSON.stringify(true));
}, [accountMnemoinc, masterInfoObject.uuid]);
useEffect(() => {
if (!didGetToHomepage) return;
(async () => {
const giftList = await updateGiftList();
await checkForRefunds(giftList);
await handleGiftRestoreOnDomeseday(giftList);
})();
}, [didGetToHomepage]);
const { giftsArray, expiredGiftsArray } = useMemo(() => {
const now = Date.now();
const all = [];
const expired = [];
Object.values(state.gifts).forEach(gift => {
all.push(gift);
if (
now >= gift.expireTime &&
gift.state?.toLowerCase() !== 'claimed' &&
gift.state?.toLowerCase() !== 'reclaimed'
) {
expired.push(gift);
}
});
return {
giftsArray: all.sort((a, b) => b.giftNum - a.giftNum),
expiredGiftsArray: expired.sort((a, b) => b.giftNum - a.giftNum),
};
}, [state.gifts]);
const contextValue = useMemo(
() => ({
...state,
saveGiftToCloud,
bulkSaveGiftsToCloud,
checkForRefunds,
deleteGiftFromCloudAndLocal,
bulkDeleteGiftsFromCloudAndLocal,
updateGiftList,
giftsArray,
expiredGiftsArray,
}),
[
state,
saveGiftToCloud,
bulkSaveGiftsToCloud,
checkForRefunds,
deleteGiftFromCloudAndLocal,
bulkDeleteGiftsFromCloudAndLocal,
updateGiftList,
giftsArray,
expiredGiftsArray,
],
);
return (
<GiftContext.Provider value={contextValue}>{children}</GiftContext.Provider>
);
}
export const useGifts = () => useContext(GiftContext);