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 => { // Gift is expired if (item.expireTime < now) { await saveGiftLocal(item); return item; } else { // Active gift - re-derive the seed to re-encrypt for sharing let giftWalletMnemonic; if (item.createdTime > GIFT_DERIVE_PATH_CUTOFF) { giftWalletMnemonic = await deriveSparkGiftMnemonic( accountMnemoinc, item.giftNum, ); } else { giftWalletMnemonic = await deriveKeyFromMnemonic( accountMnemoinc, item.giftNum, ); } // 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 ( {children} ); } export const useGifts = () => useContext(GiftContext);