@buoy-gg/storage 7.0.41 → 7.0.43

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 (180) hide show
  1. package/README.md +11 -90
  2. package/lib/commonjs/index.js +1 -353
  3. package/lib/commonjs/preset.js +1 -85
  4. package/lib/commonjs/storage/components/DiffViewer/TreeDiffViewer.js +1 -36
  5. package/lib/commonjs/storage/components/DiffViewer/modes/ThemedSplitView.js +1 -12
  6. package/lib/commonjs/storage/components/DiffViewer/themes/diffThemes.js +1 -24
  7. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +3 -1259
  8. package/lib/commonjs/storage/components/MMKVInstanceInfoPanel.js +1 -257
  9. package/lib/commonjs/storage/components/MMKVInstanceSelector.js +1 -419
  10. package/lib/commonjs/storage/components/SelectionActionBar.js +1 -207
  11. package/lib/commonjs/storage/components/StorageActionButtons.js +1 -242
  12. package/lib/commonjs/storage/components/StorageBrowserMode.js +1 -61
  13. package/lib/commonjs/storage/components/StorageEventActionButton.js +1 -121
  14. package/lib/commonjs/storage/components/StorageEventCard.js +1 -118
  15. package/lib/commonjs/storage/components/StorageEventDetailContent.js +1 -494
  16. package/lib/commonjs/storage/components/StorageEventFilterView.js +1 -160
  17. package/lib/commonjs/storage/components/StorageEventsSection.js +1 -24
  18. package/lib/commonjs/storage/components/StorageFilterCards.js +1 -231
  19. package/lib/commonjs/storage/components/StorageKeyCard.js +1 -520
  20. package/lib/commonjs/storage/components/StorageKeyDetailScreen.js +1 -579
  21. package/lib/commonjs/storage/components/StorageKeyRow.js +1 -559
  22. package/lib/commonjs/storage/components/StorageKeySection.js +1 -160
  23. package/lib/commonjs/storage/components/StorageKeyStats.js +1 -345
  24. package/lib/commonjs/storage/components/StorageModalWithTabs.js +1 -954
  25. package/lib/commonjs/storage/components/StorageSaveNotice.js +1 -171
  26. package/lib/commonjs/storage/components/StorageSection.js +1 -43
  27. package/lib/commonjs/storage/components/StorageTypeFilterBar.js +1 -144
  28. package/lib/commonjs/storage/hooks/useAsyncStorageKeys.js +1 -141
  29. package/lib/commonjs/storage/hooks/useMMKVInstances.js +1 -210
  30. package/lib/commonjs/storage/hooks/useMMKVKeys.js +1 -376
  31. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +1 -202
  32. package/lib/commonjs/storage/hooks/useStorageEvents.js +1 -61
  33. package/lib/commonjs/storage/stores/storageEventStore.js +1 -312
  34. package/lib/commonjs/storage/sync/storageSyncAdapter.js +1 -492
  35. package/lib/commonjs/storage/types.js +1 -5
  36. package/lib/commonjs/storage/utils/AsyncStorageListener.js +1 -691
  37. package/lib/commonjs/storage/utils/AsyncStorageListener.web.js +1 -89
  38. package/lib/commonjs/storage/utils/MMKVInstanceRegistry.js +1 -203
  39. package/lib/commonjs/storage/utils/MMKVListener.js +1 -416
  40. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +1 -200
  41. package/lib/commonjs/storage/utils/asyncStorageCompat.js +1 -89
  42. package/lib/commonjs/storage/utils/browserStorageLifecycle.js +1 -10
  43. package/lib/commonjs/storage/utils/browserStorageLifecycle.web.js +1 -70
  44. package/lib/commonjs/storage/utils/clearAllStorage.js +1 -48
  45. package/lib/commonjs/storage/utils/deleteStorageKeys.js +1 -82
  46. package/lib/commonjs/storage/utils/lineDiff.js +1 -30
  47. package/lib/commonjs/storage/utils/mmkvAvailability.js +1 -62
  48. package/lib/commonjs/storage/utils/mmkvAvailability.web.js +1 -21
  49. package/lib/commonjs/storage/utils/mmkvTypeDetection.js +1 -154
  50. package/lib/commonjs/storage/utils/requiredStorageKeyScope.js +1 -17
  51. package/lib/commonjs/storage/utils/setStorageValue.js +1 -218
  52. package/lib/commonjs/storage/utils/snapshotProvider.js +1 -382
  53. package/lib/commonjs/storage/utils/storageActionHelpers.js +1 -46
  54. package/lib/commonjs/storage/utils/storageBrowserRefresh.js +1 -49
  55. package/lib/commonjs/storage/utils/storageLabels.js +1 -13
  56. package/lib/commonjs/storage/utils/storageLabels.web.js +1 -13
  57. package/lib/commonjs/storage/utils/storageOps.js +1 -53
  58. package/lib/commonjs/storage/utils/storageQueryUtils.js +1 -35
  59. package/lib/commonjs/storage/utils/storageTimeTravelUtils.js +1 -288
  60. package/lib/commonjs/storage/utils/valueType.js +1 -83
  61. package/lib/module/index.js +1 -114
  62. package/lib/module/preset.js +1 -82
  63. package/lib/module/storage/components/DiffViewer/TreeDiffViewer.js +1 -32
  64. package/lib/module/storage/components/DiffViewer/modes/ThemedSplitView.js +1 -9
  65. package/lib/module/storage/components/DiffViewer/themes/diffThemes.js +1 -10
  66. package/lib/module/storage/components/GameUIStorageBrowser.js +3 -1257
  67. package/lib/module/storage/components/MMKVInstanceInfoPanel.js +1 -253
  68. package/lib/module/storage/components/MMKVInstanceSelector.js +1 -415
  69. package/lib/module/storage/components/SelectionActionBar.js +1 -204
  70. package/lib/module/storage/components/StorageActionButtons.js +1 -238
  71. package/lib/module/storage/components/StorageBrowserMode.js +1 -57
  72. package/lib/module/storage/components/StorageEventActionButton.js +1 -118
  73. package/lib/module/storage/components/StorageEventCard.js +1 -113
  74. package/lib/module/storage/components/StorageEventDetailContent.js +1 -490
  75. package/lib/module/storage/components/StorageEventFilterView.js +1 -156
  76. package/lib/module/storage/components/StorageEventsSection.js +1 -20
  77. package/lib/module/storage/components/StorageFilterCards.js +1 -227
  78. package/lib/module/storage/components/StorageKeyCard.js +1 -517
  79. package/lib/module/storage/components/StorageKeyDetailScreen.js +1 -577
  80. package/lib/module/storage/components/StorageKeyRow.js +1 -554
  81. package/lib/module/storage/components/StorageKeySection.js +1 -155
  82. package/lib/module/storage/components/StorageKeyStats.js +1 -342
  83. package/lib/module/storage/components/StorageModalWithTabs.js +1 -949
  84. package/lib/module/storage/components/StorageSaveNotice.js +1 -168
  85. package/lib/module/storage/components/StorageSection.js +1 -40
  86. package/lib/module/storage/components/StorageTypeFilterBar.js +1 -139
  87. package/lib/module/storage/hooks/useAsyncStorageKeys.js +1 -136
  88. package/lib/module/storage/hooks/useMMKVInstances.js +1 -204
  89. package/lib/module/storage/hooks/useMMKVKeys.js +1 -371
  90. package/lib/module/storage/hooks/useSecureStoreKeys.js +1 -199
  91. package/lib/module/storage/hooks/useStorageEvents.js +1 -58
  92. package/lib/module/storage/stores/storageEventStore.js +1 -301
  93. package/lib/module/storage/sync/storageSyncAdapter.js +1 -486
  94. package/lib/module/storage/types.js +1 -3
  95. package/lib/module/storage/utils/AsyncStorageListener.js +1 -679
  96. package/lib/module/storage/utils/AsyncStorageListener.web.js +1 -76
  97. package/lib/module/storage/utils/MMKVInstanceRegistry.js +1 -197
  98. package/lib/module/storage/utils/MMKVListener.js +1 -403
  99. package/lib/module/storage/utils/SecureStoreRegistry.js +1 -194
  100. package/lib/module/storage/utils/asyncStorageCompat.js +1 -81
  101. package/lib/module/storage/utils/browserStorageLifecycle.js +1 -6
  102. package/lib/module/storage/utils/browserStorageLifecycle.web.js +1 -66
  103. package/lib/module/storage/utils/clearAllStorage.js +1 -43
  104. package/lib/module/storage/utils/deleteStorageKeys.js +1 -78
  105. package/lib/module/storage/utils/lineDiff.js +1 -12
  106. package/lib/module/storage/utils/mmkvAvailability.js +1 -56
  107. package/lib/module/storage/utils/mmkvAvailability.web.js +1 -15
  108. package/lib/module/storage/utils/mmkvTypeDetection.js +1 -148
  109. package/lib/module/storage/utils/requiredStorageKeyScope.js +1 -13
  110. package/lib/module/storage/utils/setStorageValue.js +1 -209
  111. package/lib/module/storage/utils/snapshotProvider.js +1 -380
  112. package/lib/module/storage/utils/storageActionHelpers.js +1 -42
  113. package/lib/module/storage/utils/storageBrowserRefresh.js +1 -44
  114. package/lib/module/storage/utils/storageLabels.js +1 -9
  115. package/lib/module/storage/utils/storageLabels.web.js +1 -9
  116. package/lib/module/storage/utils/storageOps.js +1 -46
  117. package/lib/module/storage/utils/storageQueryUtils.js +1 -30
  118. package/lib/module/storage/utils/storageTimeTravelUtils.js +1 -281
  119. package/lib/module/storage/utils/valueType.js +1 -76
  120. package/lib/web/index.mjs +2 -3
  121. package/package.json +6 -10
  122. package/lib/typescript/index.d.ts.map +0 -1
  123. package/lib/typescript/preset.d.ts.map +0 -1
  124. package/lib/typescript/storage/components/DiffViewer/TreeDiffViewer.d.ts.map +0 -1
  125. package/lib/typescript/storage/components/DiffViewer/modes/ThemedSplitView.d.ts.map +0 -1
  126. package/lib/typescript/storage/components/DiffViewer/themes/diffThemes.d.ts.map +0 -1
  127. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +0 -1
  128. package/lib/typescript/storage/components/MMKVInstanceInfoPanel.d.ts.map +0 -1
  129. package/lib/typescript/storage/components/MMKVInstanceSelector.d.ts.map +0 -1
  130. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +0 -1
  131. package/lib/typescript/storage/components/StorageActionButtons.d.ts.map +0 -1
  132. package/lib/typescript/storage/components/StorageBrowserMode.d.ts.map +0 -1
  133. package/lib/typescript/storage/components/StorageEventActionButton.d.ts.map +0 -1
  134. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +0 -1
  135. package/lib/typescript/storage/components/StorageEventDetailContent.d.ts.map +0 -1
  136. package/lib/typescript/storage/components/StorageEventFilterView.d.ts.map +0 -1
  137. package/lib/typescript/storage/components/StorageEventsSection.d.ts.map +0 -1
  138. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +0 -1
  139. package/lib/typescript/storage/components/StorageKeyCard.d.ts.map +0 -1
  140. package/lib/typescript/storage/components/StorageKeyDetailScreen.d.ts.map +0 -1
  141. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +0 -1
  142. package/lib/typescript/storage/components/StorageKeySection.d.ts.map +0 -1
  143. package/lib/typescript/storage/components/StorageKeyStats.d.ts.map +0 -1
  144. package/lib/typescript/storage/components/StorageModalWithTabs.d.ts.map +0 -1
  145. package/lib/typescript/storage/components/StorageSaveNotice.d.ts.map +0 -1
  146. package/lib/typescript/storage/components/StorageSection.d.ts.map +0 -1
  147. package/lib/typescript/storage/components/StorageTypeFilterBar.d.ts.map +0 -1
  148. package/lib/typescript/storage/hooks/useAsyncStorageKeys.d.ts.map +0 -1
  149. package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +0 -1
  150. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +0 -1
  151. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +0 -1
  152. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +0 -1
  153. package/lib/typescript/storage/stores/storageEventStore.d.ts.map +0 -1
  154. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +0 -1
  155. package/lib/typescript/storage/types.d.ts.map +0 -1
  156. package/lib/typescript/storage/utils/AsyncStorageListener.d.ts.map +0 -1
  157. package/lib/typescript/storage/utils/AsyncStorageListener.web.d.ts.map +0 -1
  158. package/lib/typescript/storage/utils/MMKVInstanceRegistry.d.ts.map +0 -1
  159. package/lib/typescript/storage/utils/MMKVListener.d.ts.map +0 -1
  160. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +0 -1
  161. package/lib/typescript/storage/utils/asyncStorageCompat.d.ts.map +0 -1
  162. package/lib/typescript/storage/utils/browserStorageLifecycle.d.ts.map +0 -1
  163. package/lib/typescript/storage/utils/browserStorageLifecycle.web.d.ts.map +0 -1
  164. package/lib/typescript/storage/utils/clearAllStorage.d.ts.map +0 -1
  165. package/lib/typescript/storage/utils/deleteStorageKeys.d.ts.map +0 -1
  166. package/lib/typescript/storage/utils/lineDiff.d.ts.map +0 -1
  167. package/lib/typescript/storage/utils/mmkvAvailability.d.ts.map +0 -1
  168. package/lib/typescript/storage/utils/mmkvAvailability.web.d.ts.map +0 -1
  169. package/lib/typescript/storage/utils/mmkvTypeDetection.d.ts.map +0 -1
  170. package/lib/typescript/storage/utils/requiredStorageKeyScope.d.ts.map +0 -1
  171. package/lib/typescript/storage/utils/setStorageValue.d.ts.map +0 -1
  172. package/lib/typescript/storage/utils/snapshotProvider.d.ts.map +0 -1
  173. package/lib/typescript/storage/utils/storageActionHelpers.d.ts.map +0 -1
  174. package/lib/typescript/storage/utils/storageBrowserRefresh.d.ts.map +0 -1
  175. package/lib/typescript/storage/utils/storageLabels.d.ts.map +0 -1
  176. package/lib/typescript/storage/utils/storageLabels.web.d.ts.map +0 -1
  177. package/lib/typescript/storage/utils/storageOps.d.ts.map +0 -1
  178. package/lib/typescript/storage/utils/storageQueryUtils.d.ts.map +0 -1
  179. package/lib/typescript/storage/utils/storageTimeTravelUtils.d.ts.map +0 -1
  180. package/lib/typescript/storage/utils/valueType.d.ts.map +0 -1
@@ -1,1258 +1,4 @@
1
- "use strict";
1
+ "use strict";import{storageLabels as wt}from"../utils/storageLabels";import{useMemo as S,useCallback as b,useState as N,useEffect as A,useRef as ie}from"react";import{StyleSheet as It,Text as m,View as f,TouchableOpacity as ae,ScrollView as Re,Alert as le}from"react-native";import{absoluteFill as Ae,useLimit as Ee,TIER_LIMITS as _t,monoFont as W}from"@buoy-gg/shared-ui";import{Database as Le,Zap as Mt,ProBadge as ze,ProUpgradeModal as Kt,ToolEmptyState as xt,resolveListEmptyState as Bt}from"@buoy-gg/shared-ui";import{isDevToolsStorageKey as p}from"@buoy-gg/shared-ui";import{StorageKeySection as Nt,storageRowId as De}from"./StorageKeySection";import{StorageKeyDetailScreen as qt}from"./StorageKeyDetailScreen";import{StorageFilterCards as Vt}from"./StorageFilterCards";import{useAsyncStorageKeys as Rt}from"../hooks/useAsyncStorageKeys";import{addListener as At}from"../utils/AsyncStorageListener";import{useMultiMMKVKeys as Et}from"../hooks/useMMKVKeys";import{useSecureStoreKeys as Lt}from"../hooks/useSecureStoreKeys";import{useMMKVInstances as zt}from"../hooks/useMMKVInstances";import{StorageSaveNotice as Dt}from"./StorageSaveNotice";import{StorageActionButtons as Ft}from"./StorageActionButtons";import{SelectionActionBar as Wt}from"./SelectionActionBar";import{addMMKVListener as Ot}from"../utils/MMKVListener";import{onStorageBrowserRefresh as Ht}from"../utils/storageBrowserRefresh";import{deleteStorageKeys as Pt}from"../utils/deleteStorageKeys";import{setStorageValue as $t,storageEditBlockedReason as Ut}from"../utils/setStorageValue";import{gameUIColors as E,macOSColors as J,night as s,HardDrive as Fe}from"@buoy-gg/shared-ui";import{useIsPro as jt}from"@buoy-gg/license";import{jsx as a,jsxs as d,Fragment as Gt}from"react/jsx-runtime";const We=[J.semantic.info,J.semantic.success,J.semantic.warning,J.semantic.debug,"#FF6B9D","#00D9FF"];function L(z){const ce=z.split("").reduce((O,de)=>O+de.charCodeAt(0),0);return We[ce%We.length]}export const FREE_TIER_KEY_LIMIT=_t["storage.keys"].free;export function GameUIStorageBrowser({requiredStorageKeys:z=[],showFilters:ce=false,ignoredPatterns:O=new Set(["@react_buoy"]),onTogglePattern:de,onAddPattern:ue,searchQuery:w="",storageDataRef:X,eventCountByKey:ge,lastWriteByKey:Yt,onViewHistory:Z,enabledStorageTypes:H,statusFilter:ye,onStatusFilterChange:me,onStatusCountsChange:fe,pinnedKeys:_,onTogglePin:he,storageTypeFilter:pe,onStorageTypeFilterChange:Se,applySafeAreaInset:Oe,detailKey:P=null,onOpenKey:He,onCloseDetail:be}){const M=jt();const $=Ee("storage.keys");const Ce=Ee("storage.mmkvInstances");const[Q,q]=N(false);A(()=>{if(Q&&M){q(false)}},[Q,M]);const[Pe,$e]=N("all");const ee=ye!=null&&me!=null;const T=ee?ye:Pe;const Ue=ee?me:$e;const[je,Ge]=N("all");const ve=pe!=null&&Se!=null;const C=ve?pe:je;const Ye=ve?Se:Ge;const[g,Te]=N(null);const Je=ie(null);const Xe=ie(0);const ke=ie(new Map);const Ze=b((e,t)=>{if(t)ke.current.set(e,t);else ke.current.delete(e)},[]);const[we,Ie]=N(0);const[te,re]=N(false);const[U,V]=N(new Set);const{instances:y}=zt(false);const{storageKeys:_e,isLoading:Qe,error:et,refresh:j}=Rt(z);const tt=S(()=>y.map(e=>({instance:e.instance,id:e.id})),[y]);const{storageKeys:oe,isLoading:rt,error:ot,refresh:G}=Et(tt,z);const Me=S(()=>{if(!g){return oe}return oe.filter(e=>e.instanceId===g)},[oe,g]);const{storageKeys:Ke,isLoading:nt,error:st,refresh:xe}=Lt(z);const c=S(()=>{return[..._e,...Me,...Ke]},[_e,Me,Ke]);const K=S(()=>{if(!P)return null;const e=De(P);return c.find(t=>De(t)===e)??P},[P,c]);const it=Qe||rt||nt;const Be=et||ot||st;const k=b(()=>{j();G();xe()},[j,G,xe]);const R=b(e=>{return e.instanceId?`${e.storageType}-${e.instanceId}-${e.key}`:`${e.storageType}-${e.key}`},[]);const at=b(()=>{re(e=>{if(e){V(new Set)}return!e})},[]);const lt=b((e,t)=>{const r=R(e);V(h=>{const i=new Set(h);if(t){i.add(r)}else{i.delete(r)}return i})},[R]);const ct=b(()=>{V(new Set);re(false);k()},[k]);const ne=b(e=>{e.forEach(t=>ue?.(t.key));V(new Set);re(false)},[ue]);const se=b(async(e,t)=>{if(!M){q(true);throw new Error("Editing storage values is a Pro feature.")}await $t(e,t,y);k();Ie(r=>r+1)},[M,y,k]);const dt=b(e=>{void se(e,JSON.stringify(!e.value)).catch(t=>{le.alert("Couldn't save",t instanceof Error?t.message:String(t))})},[se]);const Ne=b((e,t)=>{if(!M){q(true);return}le.alert("Delete key",`Delete "${e.key}"? This cannot be undone.`,[{text:"Cancel",style:"cancel"},{text:"Delete",style:"destructive",onPress:async()=>{try{await Pt([e],y.map(r=>({id:r.id,instance:r.instance})));k();t?.()}catch(r){le.alert("Error",`Failed to delete key: ${r}`)}}}])},[M,y,k]);const qe=b(e=>Ut(e,y),[y]);const ut=S(()=>{const e=c;const t={valid:e.filter(l=>l.status==="required_present"||l.status==="optional_present").length,missing:e.filter(l=>l.status==="required_missing").length,issues:e.filter(l=>l.status==="required_wrong_value"||l.status==="required_wrong_type").length,total:e.length};const r=e.filter(l=>l.storageType==="async");const h=e.filter(l=>l.storageType==="mmkv");const i=e.filter(l=>l.storageType==="secure");return{summary:{valid:t.valid,missing:t.missing,issues:t.issues,total:t.total,timestamp:new Date().toISOString()},asyncStorage:r.reduce((l,u)=>{l[u.key]=u.value;return l},{}),mmkv:h.reduce((l,u)=>{const I=u.instanceId||"default";if(!l[I])l[I]={};l[I][u.key]=u.value;return l},{}),secure:i.reduce((l,u)=>{l[u.key]=u.value;return l},{})}},[c]);A(()=>{if(X){X.current=c}},[c,X]);A(()=>{let e;const t=At(r=>{clearTimeout(e);e=setTimeout(()=>{j()},100)});return()=>{clearTimeout(e);t()}},[j]);A(()=>{let e;const t=Ht(()=>{clearTimeout(e);e=setTimeout(k,100)});return()=>{clearTimeout(e);t()}},[k]);A(()=>{let e;const t=Ot(r=>{const h=g===null||r.instanceId===g;if(h){clearTimeout(e);e=setTimeout(()=>{G()},100)}});return()=>{clearTimeout(e);t()}},[G,g]);const D=S(()=>{const e=c;const t=e.filter(i=>!p(i.key));const r=e.filter(i=>p(i.key));const h={totalCount:e.length,requiredCount:t.filter(i=>i.category==="required").length,missingCount:t.filter(i=>i.status==="required_missing").length,wrongValueCount:t.filter(i=>i.status==="required_wrong_value").length,wrongTypeCount:t.filter(i=>i.status==="required_wrong_type").length,presentRequiredCount:t.filter(i=>i.status==="required_present").length,optionalCount:t.filter(i=>i.category==="optional").length,mmkvCount:e.filter(i=>i.storageType==="mmkv").length,asyncCount:e.filter(i=>i.storageType==="async").length,secureCount:e.filter(i=>i.storageType==="secure").length,devToolsCount:r.length};return h},[c]);const x=S(()=>{let e=c.filter(o=>!p(o.key));if(C!=="all"){e=e.filter(o=>o.storageType===C)}const t=e.filter(o=>o.status==="required_present"||o.status==="optional_present").length;const r=e.filter(o=>o.status==="required_missing").length;const h=e.filter(o=>o.status==="required_wrong_type"||o.status==="required_wrong_value").length;let i=0;let l=0;let u=0;let I=0;if(T==="all"){i=c.filter(o=>!p(o.key)&&o.storageType==="async"&&(o.status==="required_present"||o.status==="optional_present")).length;l=c.filter(o=>!p(o.key)&&o.storageType==="mmkv"&&(o.status==="required_present"||o.status==="optional_present")).length;u=c.filter(o=>!p(o.key)&&o.storageType==="secure"&&(o.status==="required_present"||o.status==="optional_present")).length;I=i+l+u}else if(T==="missing"){i=c.filter(o=>!p(o.key)&&o.storageType==="async"&&o.status==="required_missing").length;l=c.filter(o=>!p(o.key)&&o.storageType==="mmkv"&&o.status==="required_missing").length;u=c.filter(o=>!p(o.key)&&o.storageType==="secure"&&o.status==="required_missing").length;I=i+l+u}else if(T==="issues"){i=c.filter(o=>!p(o.key)&&o.storageType==="async"&&(o.status==="required_wrong_type"||o.status==="required_wrong_value")).length;l=c.filter(o=>!p(o.key)&&o.storageType==="mmkv"&&(o.status==="required_wrong_type"||o.status==="required_wrong_value")).length;u=c.filter(o=>!p(o.key)&&o.storageType==="secure"&&(o.status==="required_wrong_type"||o.status==="required_wrong_value")).length;I=i+l+u}return{validCount:t,missingCount:r,issuesCount:h,asyncCount:i,mmkvCount:l,secureCount:u,totalCount:I}},[c,T,C]);A(()=>{fe?.({validCount:x.validCount,missingCount:x.missingCount,issuesCount:x.issuesCount})},[fe,x.validCount,x.missingCount,x.issuesCount]);const Ve=S(()=>{return[...c].sort((e,t)=>{const r={required_missing:1,required_wrong_type:2,required_wrong_value:3,required_present:4,optional_present:5};return(r[e.status]||999)-(r[t.status]||999)})},[c]);const Jt=S(()=>{return Array.from(new Set(c.map(e=>e.key))).sort()},[c]);const v=S(()=>{let e=Ve;if(H&&H.size>0){e=e.filter(t=>H.has(t.storageType))}e=e.filter(t=>{const r=Array.from(O).some(h=>t.key.includes(h));return!r});if(w){const t=w.toLowerCase();e=e.filter(r=>r.key.toLowerCase().includes(t))}switch(T){case"all":e=e.filter(t=>t.status==="required_present"||t.status==="optional_present");break;case"missing":e=e.filter(t=>t.status==="required_missing");break;case"issues":e=e.filter(t=>t.status==="required_wrong_type"||t.status==="required_wrong_value");break}if(C!=="all"){e=e.filter(t=>t.storageType===C)}if(_&&_.size>0){const t=e.filter(h=>_.has(h.key));const r=e.filter(h=>!_.has(h.key));e=[...t,...r]}return e},[Ve,T,C,O,w,g,H,_]);const B=S(()=>v.slice(0,$),[v,$]);const Y=S(()=>{return Math.max(0,v.length-$)},[v.length,$]);const gt=Y>0;const Xt=S(()=>{const e=v.filter(r=>!p(r.key));const t={totalCount:v.length,requiredCount:e.filter(r=>r.category==="required").length,missingCount:e.filter(r=>r.status==="required_missing").length,wrongValueCount:e.filter(r=>r.status==="required_wrong_value").length,wrongTypeCount:e.filter(r=>r.status==="required_wrong_type").length,presentRequiredCount:e.filter(r=>r.status==="required_present").length,optionalCount:e.filter(r=>r.category==="optional").length,mmkvCount:v.filter(r=>r.storageType==="mmkv").length,asyncCount:v.filter(r=>r.storageType==="async").length,secureCount:v.filter(r=>r.storageType==="secure").length,devToolsCount:v.filter(r=>p(r.key)).length};return t},[v]);const yt=S(()=>{return B.filter(e=>U.has(R(e)))},[B,U,R]);const mt=b(()=>{const e=new Set(B.map(R));V(e)},[B,R]);const ft=b(()=>{V(new Set)},[]);const F=D.requiredCount>0?Math.round(D.presentRequiredCount/D.requiredCount*100):D.totalCount>0?100:0;const ht=F>=90?"OPTIMAL":F>=70?"WARNING":"CRITICAL";const pt=F>=90?E.success:F>=70?E.warning:E.error;if(K){return a(f,{style:n.root,children:a(qt,{storageKey:K,editBlockedReason:qe(K),eventCount:ge?.[K.key],onSave:e=>se(K,e),onViewHistory:Z?()=>Z(K.key):void 0,onDeleteKey:e=>Ne(e,()=>be?.()),onHideKey:e=>{ne([e]);be?.()},isPinned:_?.has(K.key)??false,onTogglePin:he,applySafeAreaInset:Oe})})}if(it&&c.length===0){return d(f,{style:n.emptyState,children:[a(Le,{size:48,color:s.color.textTertiary}),a(m,{style:n.emptyTitle,children:"Loading storage keys..."})]})}if(Be){return d(f,{style:n.emptyState,children:[a(Le,{size:48,color:E.error}),a(m,{style:[n.emptyTitle,{color:E.error}],children:"Error loading storage"}),a(m,{style:n.emptySubtitle,children:Be.message})]})}const St=d(Gt,{children:[C==="mmkv"&&y.length===0&&d(f,{style:n.emptyMMKVState,children:[a(m,{style:n.emptyMMKVTitle,children:"No MMKV Instances Detected"}),a(m,{style:n.emptyMMKVSubtitle,children:"MMKV instances must be registered with registerMMKVInstance()"}),a(m,{style:n.emptyMMKVCode,children:`import { registerMMKVInstance } from '@buoy-gg/storage';
2
2
 
3
- import { storageLabels } from "../utils/storageLabels";
4
- import { useMemo, useCallback, useState, useEffect, useRef } from "react";
5
- import { StyleSheet, Text, View, TouchableOpacity, ScrollView, Alert } from "react-native";
6
- import { absoluteFill, useLimit, TIER_LIMITS, monoFont } from "@buoy-gg/shared-ui";
7
- import { Database, Zap, ProBadge, ProUpgradeModal, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
8
- import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
9
- import { StorageKeySection, storageRowId } from "./StorageKeySection";
10
- import { StorageKeyDetailScreen } from "./StorageKeyDetailScreen";
11
- import { StorageFilterCards } from "./StorageFilterCards";
12
- import { useAsyncStorageKeys } from "../hooks/useAsyncStorageKeys";
13
- import { addListener } from "../utils/AsyncStorageListener";
14
- import { useMultiMMKVKeys } from "../hooks/useMMKVKeys";
15
- import { useSecureStoreKeys } from "../hooks/useSecureStoreKeys";
16
- import { useMMKVInstances } from "../hooks/useMMKVInstances";
17
- import { StorageSaveNotice } from "./StorageSaveNotice";
18
- import { StorageActionButtons } from "./StorageActionButtons";
19
- import { SelectionActionBar } from "./SelectionActionBar";
20
- // Pure JS (no react-native-mmkv dependency) — works with any registered
21
- // instance, including remote-backed wrappers on the desktop dashboard.
22
- import { addMMKVListener } from "../utils/MMKVListener";
23
- import { onStorageBrowserRefresh } from "../utils/storageBrowserRefresh";
24
- import { deleteStorageKeys } from "../utils/deleteStorageKeys";
25
- import { setStorageValue, storageEditBlockedReason } from "../utils/setStorageValue";
26
-
27
- // Import shared Game UI components
28
- import { gameUIColors, macOSColors, night, HardDrive } from "@buoy-gg/shared-ui";
29
- import { useIsPro } from "@buoy-gg/license";
30
-
31
- // MMKV Instance color palette - consistent colors per instance
32
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
33
- const INSTANCE_COLORS = [macOSColors.semantic.info,
34
- // Blue
35
- macOSColors.semantic.success,
36
- // Green
37
- macOSColors.semantic.warning,
38
- // Orange
39
- macOSColors.semantic.debug,
40
- // Purple
41
- '#FF6B9D',
42
- // Pink
43
- '#00D9FF' // Cyan
44
- ];
45
-
46
- /**
47
- * Get consistent color for an MMKV instance based on its ID
48
- * Uses simple hash to ensure same instance always gets same color
49
- */
50
- function getInstanceColor(instanceId) {
51
- const hash = instanceId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
52
- return INSTANCE_COLORS[hash % INSTANCE_COLORS.length];
53
- }
54
-
55
- /**
56
- * @deprecated Read the live limit with `useLimit("storage.keys")` — this only
57
- * reports the FREE tier's value and is blind to the anonymous tier. Kept
58
- * because it is part of the package's public exports.
59
- */
60
- export const FREE_TIER_KEY_LIMIT = TIER_LIMITS["storage.keys"].free;
61
- export function GameUIStorageBrowser({
62
- requiredStorageKeys = [],
63
- showFilters = false,
64
- ignoredPatterns = new Set(["@react_buoy"]),
65
- onTogglePattern,
66
- onAddPattern,
67
- searchQuery = "",
68
- storageDataRef,
69
- eventCountByKey,
70
- lastWriteByKey,
71
- onViewHistory,
72
- enabledStorageTypes,
73
- statusFilter,
74
- onStatusFilterChange,
75
- onStatusCountsChange,
76
- pinnedKeys,
77
- onTogglePin,
78
- storageTypeFilter,
79
- onStorageTypeFilterChange,
80
- applySafeAreaInset,
81
- detailKey = null,
82
- onOpenKey,
83
- onCloseDetail
84
- }) {
85
- const isPro = useIsPro();
86
- const keyLimit = useLimit("storage.keys");
87
- const mmkvInstanceLimit = useLimit("storage.mmkvInstances");
88
- const [showUpgradeModal, setShowUpgradeModal] = useState(false);
89
-
90
- // Auto-close upgrade modal when user becomes Pro
91
- useEffect(() => {
92
- if (showUpgradeModal && isPro) {
93
- setShowUpgradeModal(false);
94
- }
95
- }, [showUpgradeModal, isPro]);
96
- const [internalFilter, setInternalFilter] = useState("all");
97
- // Controlled when the host supplies both halves; uncontrolled otherwise.
98
- const isFilterControlled = statusFilter != null && onStatusFilterChange != null;
99
- const activeFilter = isFilterControlled ? statusFilter : internalFilter;
100
- const setActiveFilter = isFilterControlled ? onStatusFilterChange : setInternalFilter;
101
- const [internalStorageType, setInternalStorageType] = useState("all");
102
- // Controlled when the host supplies both halves — the modal does, so the
103
- // Events tab shows the same backend you picked here.
104
- const isStorageTypeControlled = storageTypeFilter != null && onStorageTypeFilterChange != null;
105
- const activeStorageType = isStorageTypeControlled ? storageTypeFilter : internalStorageType;
106
- const setActiveStorageType = isStorageTypeControlled ? onStorageTypeFilterChange : setInternalStorageType;
107
- const [selectedMMKVInstance, setSelectedMMKVInstance] = useState(null);
108
- const scrollRef = useRef(null);
109
- const scrollOffsetRef = useRef(0);
110
- const rowNodesRef = useRef(new Map());
111
- const handleRowRef = useCallback((id, node) => {
112
- if (node) rowNodesRef.current.set(id, node);else rowNodesRef.current.delete(id);
113
- }, []);
114
-
115
- /**
116
- * Shown after every successful write, with the same sentence every time.
117
- *
118
- * An earlier version looked up which live store owned the key and offered to
119
- * make that store re-read it. That required knowing what is linked to what,
120
- * and every route to that knowledge is a guess — key names collide across
121
- * backends, only zustand stores register at all, and a store can register
122
- * after the lookup ran. A message that is right most of the time is the
123
- * problem this tool has, not the fix for it.
124
- *
125
- * There is also no general mechanism to find: the app owns its cache, and
126
- * only the app can invalidate it. So the tool promises the one thing that is
127
- * true for every key, every backend and every persistence library — this is
128
- * what loads next launch — and "make it live now" lives in the state tools,
129
- * where the store you mean is the one you are looking at.
130
- */
131
- /**
132
- * Bumped on every save rather than flipped to `true`.
133
- *
134
- * The toast unmounts itself when it fades, and a boolean already `true` would
135
- * not change on the next save — so a second edit would show nothing. The
136
- * counter is the toast's `key`, so each save mounts a fresh one and the
137
- * animation restarts from the beginning.
138
- */
139
- const [saveToastId, setSaveToastId] = useState(0);
140
-
141
- // Selection mode state
142
- const [isSelectMode, setIsSelectMode] = useState(false);
143
- const [selectedKeyIds, setSelectedKeyIds] = useState(new Set());
144
-
145
- // Get all MMKV instances
146
- const {
147
- instances: mmkvInstances
148
- } = useMMKVInstances(false);
149
-
150
- // REMOVED: Auto-selection is now handled by the instance navbar
151
- // Users can manually select an instance from the navbar if they want to filter by instance
152
- // By default, selectedMMKVInstance is null, which shows ALL MMKV keys
153
-
154
- // Use AsyncStorage hook
155
- const {
156
- storageKeys: asyncStorageKeys,
157
- isLoading: isLoadingAsync,
158
- error: asyncError,
159
- refresh: refreshAsync
160
- } = useAsyncStorageKeys(requiredStorageKeys);
161
-
162
- // Memoize the instances array to prevent infinite re-renders
163
- const mmkvInstancesForHook = useMemo(() => mmkvInstances.map(inst => ({
164
- instance: inst.instance,
165
- id: inst.id
166
- })), [mmkvInstances]);
167
-
168
- // Use MMKV hook - always fetch from ALL instances, then filter by selected instance
169
- const {
170
- storageKeys: allMMKVKeys,
171
- isLoading: isLoadingMMKV,
172
- error: mmkvError,
173
- refresh: refreshMMKV
174
- } = useMultiMMKVKeys(mmkvInstancesForHook, requiredStorageKeys);
175
-
176
- // Filter MMKV keys by selected instance (if one is selected)
177
- const mmkvStorageKeys = useMemo(() => {
178
- if (!selectedMMKVInstance) {
179
- return allMMKVKeys; // Show all MMKV keys
180
- }
181
- return allMMKVKeys.filter(k => k.instanceId === selectedMMKVInstance);
182
- }, [allMMKVKeys, selectedMMKVInstance]);
183
-
184
- // Use SecureStore hook — only surfaces keys declared via registerSecureStoreKeys()
185
- const {
186
- storageKeys: secureStorageKeys,
187
- isLoading: isLoadingSecure,
188
- error: secureError,
189
- refresh: refreshSecure
190
- } = useSecureStoreKeys(requiredStorageKeys);
191
-
192
- // Merge storage keys from AsyncStorage, MMKV, and SecureStore
193
- const allStorageKeys = useMemo(() => {
194
- return [...asyncStorageKeys, ...mmkvStorageKeys, ...secureStorageKeys];
195
- }, [asyncStorageKeys, mmkvStorageKeys, secureStorageKeys]);
196
-
197
- /**
198
- * The CURRENT snapshot of the key the detail screen is open on. `detailKey`
199
- * is only an identity — resolving it fresh each pass is what makes the
200
- * detail screen live: the app writes, the snapshot refreshes, the screen
201
- * follows. Falls back to the captured info because the backend hooks
202
- * transiently EMPTY their lists mid-refresh — treating "missing from this
203
- * render's list" as "deleted" closed the screen the moment it opened, so
204
- * absence here is never acted on (deletion closes explicitly, from the
205
- * delete flow itself).
206
- */
207
- const detailInfo = useMemo(() => {
208
- if (!detailKey) return null;
209
- const id = storageRowId(detailKey);
210
- return allStorageKeys.find(k => storageRowId(k) === id) ?? detailKey;
211
- }, [detailKey, allStorageKeys]);
212
-
213
- // Determine loading and error states
214
- const isLoading = isLoadingAsync || isLoadingMMKV || isLoadingSecure;
215
- const error = asyncError || mmkvError || secureError;
216
-
217
- // Combined refresh function
218
- const refresh = useCallback(() => {
219
- refreshAsync();
220
- refreshMMKV();
221
- refreshSecure();
222
- }, [refreshAsync, refreshMMKV, refreshSecure]);
223
-
224
- // Generate unique identifier for a storage key
225
- const getKeyIdentifier = useCallback(storageKey => {
226
- return storageKey.instanceId ? `${storageKey.storageType}-${storageKey.instanceId}-${storageKey.key}` : `${storageKey.storageType}-${storageKey.key}`;
227
- }, []);
228
-
229
- // Toggle select mode
230
- const handleToggleSelectMode = useCallback(() => {
231
- setIsSelectMode(prev => {
232
- if (prev) {
233
- // When exiting select mode, clear selection
234
- setSelectedKeyIds(new Set());
235
- }
236
- return !prev;
237
- });
238
- }, []);
239
-
240
- // Handle selection change for a single key
241
- const handleSelectionChange = useCallback((storageKey, selected) => {
242
- const keyId = getKeyIdentifier(storageKey);
243
- setSelectedKeyIds(prev => {
244
- const newSet = new Set(prev);
245
- if (selected) {
246
- newSet.add(keyId);
247
- } else {
248
- newSet.delete(keyId);
249
- }
250
- return newSet;
251
- });
252
- }, [getKeyIdentifier]);
253
-
254
- // Clear selection and exit select mode after deletion
255
- const handleDeleteComplete = useCallback(() => {
256
- setSelectedKeyIds(new Set());
257
- setIsSelectMode(false);
258
- refresh();
259
- }, [refresh]);
260
-
261
- // Hide the selected keys by adding each to the ignored-pattern filter store,
262
- // then exit selection mode. Non-destructive — keys can be unhidden from the
263
- // filters UI.
264
- const handleHideKeys = useCallback(keys => {
265
- keys.forEach(k => onAddPattern?.(k.key));
266
- setSelectedKeyIds(new Set());
267
- setIsSelectMode(false);
268
- }, [onAddPattern]);
269
-
270
- /**
271
- * Write an edited value back to the device.
272
- *
273
- * Pro is checked here rather than on the Edit button because the technical
274
- * blocks (`storageEditBlockedReason`) and the licence are different kinds of
275
- * "no": the former means this key can never be edited and the row says so
276
- * permanently, the latter is an upsell. Throwing lets the editor keep the
277
- * draft on screen instead of discarding what was typed.
278
- */
279
- const handleSaveValue = useCallback(async (storageKey, raw) => {
280
- if (!isPro) {
281
- setShowUpgradeModal(true);
282
- throw new Error("Editing storage values is a Pro feature.");
283
- }
284
- await setStorageValue(storageKey, raw, mmkvInstances);
285
- refresh();
286
- setSaveToastId(id => id + 1);
287
- }, [isPro, mmkvInstances, refresh]);
288
-
289
- /**
290
- * Flip a boolean key straight from its card.
291
- *
292
- * Goes through `handleSaveValue` like every other write, so the Pro gate and
293
- * the type preservation are the same ones the editor uses — this is only a
294
- * shortcut past the UI, not past the rules.
295
- */
296
- const handleToggleKey = useCallback(storageKey => {
297
- void handleSaveValue(storageKey, JSON.stringify(!storageKey.value)).catch(e => {
298
- Alert.alert("Couldn't save", e instanceof Error ? e.message : String(e));
299
- });
300
- }, [handleSaveValue]);
301
-
302
- /**
303
- * Delete ONE key, from its own card.
304
- *
305
- * Routed through `deleteStorageKeys` exactly like the selection bar, so the
306
- * per-backend fan-out keeps having a single implementation — the last time
307
- * that fan-out was wrong (an MMKV method renamed upstream) it was wrong in
308
- * silence, and a second copy is how one backend quietly stops being deleted.
309
- *
310
- * Pro is checked here rather than on the button because it's an upsell, not
311
- * a capability: the row shows the action and explains the wall on press.
312
- */
313
- const handleDeleteKey = useCallback((storageKey, onDeleted) => {
314
- if (!isPro) {
315
- setShowUpgradeModal(true);
316
- return;
317
- }
318
- Alert.alert("Delete key", `Delete "${storageKey.key}"? This cannot be undone.`, [{
319
- text: "Cancel",
320
- style: "cancel"
321
- }, {
322
- text: "Delete",
323
- style: "destructive",
324
- onPress: async () => {
325
- try {
326
- await deleteStorageKeys([storageKey], mmkvInstances.map(inst => ({
327
- id: inst.id,
328
- instance: inst.instance
329
- })));
330
- refresh();
331
- // Only after the delete actually landed — a cancelled alert
332
- // or a failed delete leaves the caller's screen alone.
333
- onDeleted?.();
334
- } catch (error) {
335
- Alert.alert("Error", `Failed to delete key: ${error}`);
336
- }
337
- }
338
- }]);
339
- }, [isPro, mmkvInstances, refresh]);
340
-
341
- /** Permanent, per-key reasons a value can't be edited (never the licence). */
342
- const editBlockedFor = useCallback(storageKey => storageEditBlockedReason(storageKey, mmkvInstances), [mmkvInstances]);
343
-
344
- // Memoized export data for copy functionality
345
- const copyExportData = useMemo(() => {
346
- const allKeys = allStorageKeys;
347
-
348
- // Calculate stats
349
- const stats = {
350
- valid: allKeys.filter(k => k.status === 'required_present' || k.status === 'optional_present').length,
351
- missing: allKeys.filter(k => k.status === 'required_missing').length,
352
- issues: allKeys.filter(k => k.status === 'required_wrong_value' || k.status === 'required_wrong_type').length,
353
- total: allKeys.length
354
- };
355
-
356
- // Group by storage type
357
- const asyncKeys = allKeys.filter(k => k.storageType === 'async');
358
- const mmkvKeys = allKeys.filter(k => k.storageType === 'mmkv');
359
- const secureKeys = allKeys.filter(k => k.storageType === 'secure');
360
-
361
- // Build structured export
362
- return {
363
- summary: {
364
- valid: stats.valid,
365
- missing: stats.missing,
366
- issues: stats.issues,
367
- total: stats.total,
368
- timestamp: new Date().toISOString()
369
- },
370
- asyncStorage: asyncKeys.reduce((acc, k) => {
371
- acc[k.key] = k.value;
372
- return acc;
373
- }, {}),
374
- mmkv: mmkvKeys.reduce((acc, k) => {
375
- const instanceId = k.instanceId || 'default';
376
- if (!acc[instanceId]) acc[instanceId] = {};
377
- acc[instanceId][k.key] = k.value;
378
- return acc;
379
- }, {}),
380
- secure: secureKeys.reduce((acc, k) => {
381
- acc[k.key] = k.value;
382
- return acc;
383
- }, {})
384
- };
385
- }, [allStorageKeys]);
386
-
387
- // Update storage data ref for copy functionality
388
- useEffect(() => {
389
- if (storageDataRef) {
390
- storageDataRef.current = allStorageKeys;
391
- }
392
- }, [allStorageKeys, storageDataRef]);
393
-
394
- // Auto-refresh on storage events (AsyncStorage)
395
- useEffect(() => {
396
- let timeoutId;
397
- const unsubscribe = addListener(event => {
398
- clearTimeout(timeoutId);
399
- timeoutId = setTimeout(() => {
400
- refreshAsync(); // Auto-refresh AsyncStorage when it changes
401
- }, 100); // 100ms debounce
402
- });
403
- return () => {
404
- clearTimeout(timeoutId);
405
- unsubscribe();
406
- };
407
- }, [refreshAsync]);
408
-
409
- // Auto-refresh when something outside this component knows the underlying
410
- // storage changed. On device the listeners above cover it; in remote-mirror
411
- // mode (desktop dashboard) local capture is suppressed and they never fire,
412
- // so the dashboard signals each incoming device snapshot this way — without
413
- // it the browse tab is frozen at whatever it read on mount.
414
- useEffect(() => {
415
- let timeoutId;
416
- const unsubscribe = onStorageBrowserRefresh(() => {
417
- clearTimeout(timeoutId);
418
- timeoutId = setTimeout(refresh, 100);
419
- });
420
- return () => {
421
- clearTimeout(timeoutId);
422
- unsubscribe();
423
- };
424
- }, [refresh]);
425
-
426
- // Auto-refresh on MMKV storage events
427
- useEffect(() => {
428
- let timeoutId;
429
- const unsubscribe = addMMKVListener(event => {
430
- // Refresh when the changed instance is one we're actually showing.
431
- // `selectedMMKVInstance` is null by default ("All instances"), so
432
- // comparing it to event.instanceId directly was never true for the view
433
- // every user starts on — MMKV values sat stale in the browse list until
434
- // it remounted, even though the write was recorded in Events.
435
- const affectsView = selectedMMKVInstance === null || event.instanceId === selectedMMKVInstance;
436
- if (affectsView) {
437
- clearTimeout(timeoutId);
438
- timeoutId = setTimeout(() => {
439
- refreshMMKV(); // Auto-refresh MMKV when it changes
440
- }, 100); // 100ms debounce
441
- }
442
- });
443
- return () => {
444
- clearTimeout(timeoutId);
445
- unsubscribe();
446
- };
447
- }, [refreshMMKV, selectedMMKVInstance]);
448
-
449
- // Calculate stats from ALL keys (not filtered) - base stats for display
450
- const stats = useMemo(() => {
451
- const allKeys = allStorageKeys;
452
- const appKeys = allKeys.filter(k => !isDevToolsStorageKey(k.key));
453
- const devKeys = allKeys.filter(k => isDevToolsStorageKey(k.key));
454
- const storageStats = {
455
- totalCount: allKeys.length,
456
- requiredCount: appKeys.filter(k => k.category === "required").length,
457
- missingCount: appKeys.filter(k => k.status === "required_missing").length,
458
- wrongValueCount: appKeys.filter(k => k.status === "required_wrong_value").length,
459
- wrongTypeCount: appKeys.filter(k => k.status === "required_wrong_type").length,
460
- presentRequiredCount: appKeys.filter(k => k.status === "required_present").length,
461
- optionalCount: appKeys.filter(k => k.category === "optional").length,
462
- // Count keys by their actual storageType property
463
- mmkvCount: allKeys.filter(k => k.storageType === "mmkv").length,
464
- asyncCount: allKeys.filter(k => k.storageType === "async").length,
465
- secureCount: allKeys.filter(k => k.storageType === "secure").length,
466
- devToolsCount: devKeys.length
467
- };
468
- return storageStats;
469
- }, [allStorageKeys]);
470
-
471
- // Calculate stats for tab badges based on current filters
472
- const tabStats = useMemo(() => {
473
- // Start with all keys, excluding devtools keys
474
- let keysForStats = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key));
475
-
476
- // If a specific storage type is selected, filter by that type first
477
- if (activeStorageType !== "all") {
478
- keysForStats = keysForStats.filter(k => k.storageType === activeStorageType);
479
- }
480
-
481
- // Calculate status counts from filtered keys
482
- const validCount = keysForStats.filter(k => k.status === "required_present" || k.status === "optional_present").length;
483
- const missingCount = keysForStats.filter(k => k.status === "required_missing").length;
484
- const issuesCount = keysForStats.filter(k => k.status === "required_wrong_type" || k.status === "required_wrong_value").length;
485
-
486
- // Calculate storage type counts based on active status filter
487
- let asyncCount = 0;
488
- let mmkvCount = 0;
489
- let secureCount = 0;
490
- let totalCount = 0;
491
- if (activeFilter === "all") {
492
- // Valid: only keys with valid status
493
- asyncCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "async" && (k.status === "required_present" || k.status === "optional_present")).length;
494
- mmkvCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "mmkv" && (k.status === "required_present" || k.status === "optional_present")).length;
495
- secureCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "secure" && (k.status === "required_present" || k.status === "optional_present")).length;
496
- totalCount = asyncCount + mmkvCount + secureCount;
497
- } else if (activeFilter === "missing") {
498
- // Missing: only keys with missing status
499
- asyncCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "async" && k.status === "required_missing").length;
500
- mmkvCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "mmkv" && k.status === "required_missing").length;
501
- secureCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "secure" && k.status === "required_missing").length;
502
- totalCount = asyncCount + mmkvCount + secureCount;
503
- } else if (activeFilter === "issues") {
504
- // Issues: only keys with issue status
505
- asyncCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "async" && (k.status === "required_wrong_type" || k.status === "required_wrong_value")).length;
506
- mmkvCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "mmkv" && (k.status === "required_wrong_type" || k.status === "required_wrong_value")).length;
507
- secureCount = allStorageKeys.filter(k => !isDevToolsStorageKey(k.key) && k.storageType === "secure" && (k.status === "required_wrong_type" || k.status === "required_wrong_value")).length;
508
- totalCount = asyncCount + mmkvCount + secureCount;
509
- }
510
- return {
511
- validCount,
512
- missingCount,
513
- issuesCount,
514
- asyncCount,
515
- mmkvCount,
516
- secureCount,
517
- totalCount
518
- };
519
- }, [allStorageKeys, activeFilter, activeStorageType]);
520
-
521
- // Push the status counts to a host toolbar that renders them (desktop).
522
- useEffect(() => {
523
- onStatusCountsChange?.({
524
- validCount: tabStats.validCount,
525
- missingCount: tabStats.missingCount,
526
- issuesCount: tabStats.issuesCount
527
- });
528
- }, [onStatusCountsChange, tabStats.validCount, tabStats.missingCount, tabStats.issuesCount]);
529
-
530
- // Sort all keys by priority (issues first)
531
- const sortedKeys = useMemo(() => {
532
- // Sort by status priority: errors first, then warnings, then valid
533
- return [...allStorageKeys].sort((a, b) => {
534
- const priorityMap = {
535
- required_missing: 1,
536
- required_wrong_type: 2,
537
- required_wrong_value: 3,
538
- required_present: 4,
539
- optional_present: 5
540
- };
541
- return (priorityMap[a.status] || 999) - (priorityMap[b.status] || 999);
542
- });
543
- }, [allStorageKeys]);
544
-
545
- // Get all unique keys for filter suggestions
546
- const allUniqueKeys = useMemo(() => {
547
- return Array.from(new Set(allStorageKeys.map(k => k.key))).sort();
548
- }, [allStorageKeys]);
549
-
550
- // Filter keys based on active filter, storage type, ignored patterns, and search
551
- const filteredKeys = useMemo(() => {
552
- let keys = sortedKeys;
553
-
554
- // Apply enabled storage types filter (from parent unified filter)
555
- if (enabledStorageTypes && enabledStorageTypes.size > 0) {
556
- keys = keys.filter(k => enabledStorageTypes.has(k.storageType));
557
- }
558
-
559
- // Apply ignored pattern filter
560
- keys = keys.filter(k => {
561
- // Check if key matches any ignored pattern
562
- const shouldIgnore = Array.from(ignoredPatterns).some(pattern => k.key.includes(pattern));
563
- return !shouldIgnore;
564
- });
565
-
566
- // Apply search filter
567
- if (searchQuery) {
568
- const query = searchQuery.toLowerCase();
569
- keys = keys.filter(k => k.key.toLowerCase().includes(query));
570
- }
571
-
572
- // Apply status filter
573
- switch (activeFilter) {
574
- case "all":
575
- // "All" (Valid) shows only keys without issues
576
- keys = keys.filter(k => k.status === "required_present" || k.status === "optional_present");
577
- break;
578
- case "missing":
579
- keys = keys.filter(k => k.status === "required_missing");
580
- break;
581
- case "issues":
582
- keys = keys.filter(k => k.status === "required_wrong_type" || k.status === "required_wrong_value");
583
- break;
584
- }
585
-
586
- // Apply storage type filter
587
- if (activeStorageType !== "all") {
588
- keys = keys.filter(k => k.storageType === activeStorageType);
589
- }
590
-
591
- // NOTE: MMKV instance filtering is now handled in mmkvStorageKeys memo (line 128)
592
- // No need to filter again here
593
-
594
- // Float pinned keys to the top (stable — preserves existing order otherwise)
595
- if (pinnedKeys && pinnedKeys.size > 0) {
596
- const pinned = keys.filter(k => pinnedKeys.has(k.key));
597
- const rest = keys.filter(k => !pinnedKeys.has(k.key));
598
- keys = [...pinned, ...rest];
599
- }
600
- return keys;
601
- }, [sortedKeys, activeFilter, activeStorageType, ignoredPatterns, searchQuery, selectedMMKVInstance, enabledStorageTypes, pinnedKeys]);
602
-
603
- // Pro is Infinity here, so slice() returns everything.
604
- const visibleKeys = useMemo(() => filteredKeys.slice(0, keyLimit), [filteredKeys, keyLimit]);
605
-
606
- // Calculate how many keys are locked (only those matching current filter)
607
- const lockedKeyCount = useMemo(() => {
608
- return Math.max(0, filteredKeys.length - keyLimit);
609
- }, [filteredKeys.length, keyLimit]);
610
- const hasLockedKeys = lockedKeyCount > 0;
611
-
612
- // Calculate stats from FILTERED keys to show actual visible counts
613
- const filteredStats = useMemo(() => {
614
- const appKeys = filteredKeys.filter(k => !isDevToolsStorageKey(k.key));
615
- const storageStats = {
616
- totalCount: filteredKeys.length,
617
- requiredCount: appKeys.filter(k => k.category === "required").length,
618
- missingCount: appKeys.filter(k => k.status === "required_missing").length,
619
- wrongValueCount: appKeys.filter(k => k.status === "required_wrong_value").length,
620
- wrongTypeCount: appKeys.filter(k => k.status === "required_wrong_type").length,
621
- presentRequiredCount: appKeys.filter(k => k.status === "required_present").length,
622
- optionalCount: appKeys.filter(k => k.category === "optional").length,
623
- mmkvCount: filteredKeys.filter(k => k.storageType === "mmkv").length,
624
- asyncCount: filteredKeys.filter(k => k.storageType === "async").length,
625
- secureCount: filteredKeys.filter(k => k.storageType === "secure").length,
626
- devToolsCount: filteredKeys.filter(k => isDevToolsStorageKey(k.key)).length
627
- };
628
- return storageStats;
629
- }, [filteredKeys]);
630
-
631
- // Get selected keys as StorageKeyInfo objects (from visible keys only)
632
- const selectedKeysInfo = useMemo(() => {
633
- return visibleKeys.filter(key => selectedKeyIds.has(getKeyIdentifier(key)));
634
- }, [visibleKeys, selectedKeyIds, getKeyIdentifier]);
635
-
636
- // Select all visible keys (only unlocked keys can be selected)
637
- const handleSelectAll = useCallback(() => {
638
- const allIds = new Set(visibleKeys.map(getKeyIdentifier));
639
- setSelectedKeyIds(allIds);
640
- }, [visibleKeys, getKeyIdentifier]);
641
-
642
- // Clear all selections
643
- const handleClearSelection = useCallback(() => {
644
- setSelectedKeyIds(new Set());
645
- }, []);
646
-
647
- /**
648
- * Serialized size of everything in storage, for the table's footer summary.
649
- * Keyed on `allStorageKeys` rather than the filtered set: it answers "how much
650
- * is in here", which a filter should not change.
651
- */
652
- // Calculate health percentage
653
- const healthPercentage = stats.requiredCount > 0 ? Math.round(stats.presentRequiredCount / stats.requiredCount * 100) : stats.totalCount > 0 ? 100 : 0;
654
- const healthStatus = healthPercentage >= 90 ? "OPTIMAL" : healthPercentage >= 70 ? "WARNING" : "CRITICAL";
655
- const healthColor = healthPercentage >= 90 ? gameUIColors.success : healthPercentage >= 70 ? gameUIColors.warning : gameUIColors.error;
656
-
657
- // The detail screen REPLACES the list as the modal's whole body — the host
658
- // modal's header carries back + the key's name, the React Query
659
- // interaction (and the Zustand tool's). All hooks above have run; this
660
- // return only swaps what's painted.
661
- if (detailInfo) {
662
- return /*#__PURE__*/_jsx(View, {
663
- style: styles.root,
664
- children: /*#__PURE__*/_jsx(StorageKeyDetailScreen, {
665
- storageKey: detailInfo,
666
- editBlockedReason: editBlockedFor(detailInfo),
667
- eventCount: eventCountByKey?.[detailInfo.key],
668
- onSave: raw => handleSaveValue(detailInfo, raw),
669
- onViewHistory: onViewHistory ? () => onViewHistory(detailInfo.key) : undefined,
670
- onDeleteKey: k => handleDeleteKey(k, () => onCloseDetail?.()),
671
- onHideKey: k => {
672
- handleHideKeys([k]);
673
- onCloseDetail?.();
674
- },
675
- isPinned: pinnedKeys?.has(detailInfo.key) ?? false,
676
- onTogglePin: onTogglePin,
677
- applySafeAreaInset: applySafeAreaInset
678
- })
679
- });
680
- }
681
-
682
- // Loading state
683
- if (isLoading && allStorageKeys.length === 0) {
684
- return /*#__PURE__*/_jsxs(View, {
685
- style: styles.emptyState,
686
- children: [/*#__PURE__*/_jsx(Database, {
687
- size: 48,
688
- color: night.color.textTertiary
689
- }), /*#__PURE__*/_jsx(Text, {
690
- style: styles.emptyTitle,
691
- children: "Loading storage keys..."
692
- })]
693
- });
694
- }
695
-
696
- // Error state
697
- if (error) {
698
- return /*#__PURE__*/_jsxs(View, {
699
- style: styles.emptyState,
700
- children: [/*#__PURE__*/_jsx(Database, {
701
- size: 48,
702
- color: gameUIColors.error
703
- }), /*#__PURE__*/_jsx(Text, {
704
- style: [styles.emptyTitle, {
705
- color: gameUIColors.error
706
- }],
707
- children: "Error loading storage"
708
- }), /*#__PURE__*/_jsx(Text, {
709
- style: styles.emptySubtitle,
710
- children: error.message
711
- })]
712
- });
713
- }
714
-
715
- /* ---------------------------------------------------------------- *
716
- * Subtrees the card list composes. Extracted rather than inlined so Pro
717
- * gating and MMKV
718
- * instance selection or the empty-state copy.
719
- * ---------------------------------------------------------------- */
720
-
721
- const mmkvSection = /*#__PURE__*/_jsxs(_Fragment, {
722
- children: [activeStorageType === "mmkv" && mmkvInstances.length === 0 && /*#__PURE__*/_jsxs(View, {
723
- style: styles.emptyMMKVState,
724
- children: [/*#__PURE__*/_jsx(Text, {
725
- style: styles.emptyMMKVTitle,
726
- children: "No MMKV Instances Detected"
727
- }), /*#__PURE__*/_jsx(Text, {
728
- style: styles.emptyMMKVSubtitle,
729
- children: "MMKV instances must be registered with registerMMKVInstance()"
730
- }), /*#__PURE__*/_jsx(Text, {
731
- style: styles.emptyMMKVCode,
732
- children: `import { registerMMKVInstance } from '@buoy-gg/storage';\n\nconst storage = createMMKV();\nregisterMMKVInstance('mmkv.default', storage);`
733
- })]
734
- }), activeStorageType === "mmkv" && mmkvInstances.length > 0 && /*#__PURE__*/_jsxs(View, {
735
- style: styles.instanceNavbar,
736
- children: [/*#__PURE__*/_jsxs(View, {
737
- style: styles.instanceNavbarHeader,
738
- children: [/*#__PURE__*/_jsx(Text, {
739
- style: styles.instanceNavbarLabel,
740
- children: "INSTANCES"
741
- }), mmkvInstances.length > mmkvInstanceLimit && /*#__PURE__*/_jsx(ProBadge, {
742
- style: {
743
- transform: [{
744
- scale: 0.85
745
- }]
746
- }
747
- })]
748
- }), /*#__PURE__*/_jsxs(ScrollView, {
749
- horizontal: true,
750
- showsHorizontalScrollIndicator: false,
751
- contentContainerStyle: styles.instanceNavbarScroll,
752
- children: [(isPro || mmkvInstances.length === 1) && /*#__PURE__*/_jsxs(TouchableOpacity, {
753
- onPress: () => setSelectedMMKVInstance(null),
754
- testID: "buoy-storage-instance-all",
755
- accessibilityRole: "button",
756
- accessibilityLabel: `All ${storageLabels.mmkv} instances`,
757
- style: [styles.instanceNavbarButton, selectedMMKVInstance === null && styles.instanceNavbarButtonActive],
758
- children: [/*#__PURE__*/_jsx(HardDrive, {
759
- size: 12,
760
- color: selectedMMKVInstance === null ? night.color.text : night.color.textSecondary
761
- }), /*#__PURE__*/_jsx(Text, {
762
- style: [styles.instanceNavbarButtonText, selectedMMKVInstance === null && styles.instanceNavbarButtonTextActive],
763
- children: "All"
764
- }), /*#__PURE__*/_jsx(View, {
765
- style: styles.instanceNavbarBadge,
766
- children: /*#__PURE__*/_jsx(Text, {
767
- style: styles.instanceNavbarBadgeText,
768
- children: mmkvInstances.reduce((sum, inst) => sum + inst.keyCount, 0)
769
- })
770
- })]
771
- }), mmkvInstances.map((inst, index) => {
772
- const isLocked = index >= mmkvInstanceLimit;
773
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
774
- testID: `buoy-storage-instance-${inst.id}`,
775
- accessibilityRole: "button",
776
- accessibilityLabel: `MMKV instance ${inst.id}`,
777
- onPress: () => {
778
- if (isLocked) {
779
- setShowUpgradeModal(true);
780
- } else {
781
- setSelectedMMKVInstance(inst.id);
782
- }
783
- },
784
- style: [styles.instanceNavbarButton, inst.id === selectedMMKVInstance && styles.instanceNavbarButtonActive, {
785
- borderColor: isLocked ? night.color.border : getInstanceColor(inst.id) + '40',
786
- backgroundColor: inst.id === selectedMMKVInstance ? getInstanceColor(inst.id) + '20' : night.color.surfaceElevated,
787
- opacity: isLocked ? 0.5 : 1
788
- }],
789
- children: [/*#__PURE__*/_jsx(HardDrive, {
790
- size: 12,
791
- color: inst.id === selectedMMKVInstance ? getInstanceColor(inst.id) : night.color.textSecondary
792
- }), /*#__PURE__*/_jsx(Text, {
793
- style: [styles.instanceNavbarButtonText, inst.id === selectedMMKVInstance && {
794
- color: getInstanceColor(inst.id),
795
- fontWeight: '700'
796
- }],
797
- children: inst.id
798
- }), /*#__PURE__*/_jsx(View, {
799
- style: [styles.instanceNavbarBadge, inst.id === selectedMMKVInstance && {
800
- backgroundColor: getInstanceColor(inst.id) + '30'
801
- }],
802
- children: /*#__PURE__*/_jsx(Text, {
803
- style: [styles.instanceNavbarBadgeText, inst.id === selectedMMKVInstance && {
804
- color: getInstanceColor(inst.id)
805
- }],
806
- children: inst.keyCount
807
- })
808
- })]
809
- }, inst.id);
810
- })]
811
- })]
812
- })]
813
- });
814
- const actionButtonsNode = /*#__PURE__*/_jsx(StorageActionButtons, {
815
- copyValue: copyExportData,
816
- mmkvInstances: mmkvInstances.map(inst => ({
817
- id: inst.id,
818
- instance: inst.instance
819
- })),
820
- activeStorageType: activeStorageType,
821
- onClearComplete: refresh,
822
- isSelectMode: isSelectMode,
823
- onToggleSelectMode: handleToggleSelectMode,
824
- selectedCount: selectedKeyIds.size
825
- });
826
-
827
- /* Selection Action Bar - Only show in select mode with selections */
828
- const selectionBarNode = isSelectMode ? /*#__PURE__*/_jsx(SelectionActionBar, {
829
- selectedKeys: selectedKeysInfo,
830
- mmkvInstances: mmkvInstances.map(inst => ({
831
- id: inst.id,
832
- instance: inst.instance
833
- })),
834
- onDeleteComplete: handleDeleteComplete,
835
- onHideKeys: handleHideKeys,
836
- onSelectAll: handleSelectAll,
837
- onClearSelection: handleClearSelection,
838
- totalVisibleKeys: visibleKeys.length
839
- }) : null;
840
- const lockedBannerNode = hasLockedKeys ? /*#__PURE__*/_jsxs(TouchableOpacity, {
841
- style: styles.limitBanner,
842
- onPress: () => setShowUpgradeModal(true),
843
- activeOpacity: 0.8,
844
- testID: "buoy-storage-locked-banner",
845
- accessibilityRole: "button",
846
- children: [/*#__PURE__*/_jsxs(View, {
847
- style: styles.limitBannerContent,
848
- children: [/*#__PURE__*/_jsxs(View, {
849
- style: styles.limitBannerLeft,
850
- children: [/*#__PURE__*/_jsx(Zap, {
851
- size: 16,
852
- color: night.color.accent
853
- }), /*#__PURE__*/_jsx(Text, {
854
- style: styles.limitBannerText,
855
- children: searchQuery ? `${lockedKeyCount} matching ${lockedKeyCount === 1 ? 'key' : 'keys'} in locked storage` : `${lockedKeyCount} keys locked`
856
- })]
857
- }), /*#__PURE__*/_jsx(ProBadge, {})]
858
- }), /*#__PURE__*/_jsx(Text, {
859
- style: styles.limitBannerSubtext,
860
- children: searchQuery ? "Upgrade to Pro to search your full storage" : "Upgrade to Pro to unlock full storage access"
861
- })]
862
- }) : null;
863
- const emptyStateNode = (() => {
864
- // Short-circuits on the first real key, so this costs nothing.
865
- const hasAnyKeys = allStorageKeys.some(k => !isDevToolsStorageKey(k.key));
866
-
867
- // Three causes here are unlike any other tool's: an empty MISSING or
868
- // ISSUES list is a PASS, not a failure, and an empty VALID list means
869
- // everything is broken. Those skip the shared resolver and say so.
870
- let content;
871
- if (searchQuery || !hasAnyKeys) {
872
- content = resolveListEmptyState({
873
- subject: "keys",
874
- verb: "registered",
875
- search: searchQuery,
876
- total: hasAnyKeys ? 1 : 0,
877
- waiting: {
878
- // Previously this fell into the "all" branch and claimed every key
879
- // had an issue — of the zero keys that exist.
880
- otto: "empty",
881
- title: "No storage keys",
882
- message: "Nothing is registered for this tool to read."
883
- }
884
- });
885
- if (searchQuery && hasAnyKeys) {
886
- // No count: the pre-search total would mean re-running the whole key
887
- // pipeline a second time for a number the sentence does not need.
888
- content = {
889
- ...content,
890
- message: "No keys match. Clear the search to see them."
891
- };
892
- }
893
- } else if (activeFilter === "missing") {
894
- content = {
895
- otto: "thumbsup",
896
- title: "No missing keys",
897
- message: "All required keys are present"
898
- };
899
- } else if (activeFilter === "issues") {
900
- content = {
901
- otto: "cool",
902
- title: "No issues found",
903
- message: "All storage keys are correctly configured"
904
- };
905
- } else {
906
- content = {
907
- otto: "broken",
908
- title: "No valid keys found",
909
- message: "All keys have issues or are missing"
910
- };
911
- }
912
- return /*#__PURE__*/_jsx(ToolEmptyState, {
913
- ...content,
914
- testID: "buoy-storage-keys-empty"
915
- });
916
- })();
917
- const upgradeModalNode = /*#__PURE__*/_jsx(ProUpgradeModal, {
918
- visible: showUpgradeModal,
919
- onClose: () => setShowUpgradeModal(false),
920
- featureName: "Storage Keys"
921
- });
922
- return /*#__PURE__*/_jsxs(View, {
923
- style: styles.root,
924
- children: [saveToastId > 0 ? /*#__PURE__*/_jsx(StorageSaveNotice, {
925
- onDone: () => setSaveToastId(0)
926
- }, saveToastId) : null, /*#__PURE__*/_jsxs(ScrollView
927
- // Named because `by.type("RCTScrollView")` doesn't match on Fabric
928
- // (renamed class) — automation scrolls below-the-fold rows by this id.
929
- , {
930
- testID: "buoy-storage-scroll",
931
- ref: scrollRef,
932
- onScroll: e => {
933
- scrollOffsetRef.current = e.nativeEvent.contentOffset.y;
934
- },
935
- scrollEventThrottle: 32,
936
- style: styles.scrollContainer,
937
- contentContainerStyle: styles.container,
938
- showsVerticalScrollIndicator: false,
939
- children: [/*#__PURE__*/_jsx(View, {
940
- style: styles.backgroundGrid
941
- }), /*#__PURE__*/_jsx(StorageFilterCards, {
942
- stats: stats,
943
- tabStats: tabStats,
944
- healthPercentage: healthPercentage,
945
- healthStatus: healthStatus,
946
- healthColor: healthColor,
947
- activeFilter: activeFilter,
948
- onFilterChange: setActiveFilter,
949
- activeStorageType: activeStorageType,
950
- onStorageTypeChange: setActiveStorageType
951
- // Same rule the wide bar follows: when a host toolbar owns the status
952
- // filter it also renders the counters, so ours would be a second copy.
953
- ,
954
- showStatus: !isFilterControlled
955
- }), mmkvSection, filteredKeys.length > 0 ? /*#__PURE__*/_jsxs(View, {
956
- style: styles.keysSection,
957
- children: [/*#__PURE__*/_jsxs(View, {
958
- style: styles.sectionHeader,
959
- children: [/*#__PURE__*/_jsxs(View, {
960
- style: styles.sectionHeaderLeft,
961
- children: [/*#__PURE__*/_jsxs(Text, {
962
- style: styles.sectionTitle,
963
- children: [activeFilter === "all" ? "KEYS" : activeFilter === "missing" ? "MISSING KEYS" : "ISSUES TO FIX", activeStorageType !== "all" && ` (${activeStorageType.toUpperCase()})`]
964
- }), /*#__PURE__*/_jsx(View, {
965
- style: styles.countBadge,
966
- children: /*#__PURE__*/_jsx(Text, {
967
- style: styles.countText,
968
- children: visibleKeys.length
969
- })
970
- })]
971
- }), actionButtonsNode]
972
- }), selectionBarNode, /*#__PURE__*/_jsx(StorageKeySection, {
973
- title: "",
974
- count: -1,
975
- keys: visibleKeys,
976
- emptyMessage: "",
977
- isSelectMode: isSelectMode,
978
- selectedKeys: selectedKeyIds,
979
- onSelectionChange: handleSelectionChange,
980
- eventCountByKey: eventCountByKey,
981
- onViewHistory: onViewHistory,
982
- onHideKey: key => handleHideKeys([key]),
983
- onToggleKey: handleToggleKey,
984
- onDeleteKey: handleDeleteKey,
985
- editBlockedFor: editBlockedFor,
986
- pinnedKeys: pinnedKeys,
987
- onTogglePin: onTogglePin,
988
- onRowRef: handleRowRef,
989
- onOpenKey: onOpenKey
990
- }), lockedBannerNode]
991
- }) : emptyStateNode, /*#__PURE__*/_jsx(Text, {
992
- style: styles.techFooter,
993
- children: "ASYNC STORAGE | MMKV | SECURE STORAGE BACKENDS"
994
- }), upgradeModalNode]
995
- })]
996
- });
997
- }
998
- const styles = StyleSheet.create({
999
- /** Measures the space the columns actually get; `flex: 1` so it fills the
1000
- * parent exactly as the bare ScrollView it wraps used to. */
1001
- root: {
1002
- flex: 1,
1003
- minHeight: 0
1004
- },
1005
- /**
1006
- * No background of its own: the tree editor paints an opaque screen, while a
1007
- * scalar key shows only a modal and wants the list visible behind its scrim.
1008
- */
1009
- editorOverlay: {
1010
- ...absoluteFill,
1011
- zIndex: 10
1012
- },
1013
- scrollContainer: {
1014
- flex: 1,
1015
- // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
1016
- backgroundColor: "transparent"
1017
- },
1018
- container: {
1019
- padding: 12,
1020
- // Generous so a key card expanded at the bottom of the list can scroll
1021
- // its full detail block above the modal edge.
1022
- paddingBottom: 120
1023
- },
1024
- backgroundGrid: {
1025
- ...absoluteFill,
1026
- opacity: 0.006,
1027
- backgroundColor: gameUIColors.info
1028
- },
1029
- techFooter: {
1030
- fontSize: 8,
1031
- color: night.color.textTertiary,
1032
- fontFamily: monoFont,
1033
- textAlign: "center",
1034
- marginTop: 20,
1035
- letterSpacing: 1,
1036
- opacity: 0.5
1037
- },
1038
- // Keys section
1039
- keysSection: {
1040
- marginTop: 8,
1041
- backgroundColor: night.color.bg,
1042
- borderRadius: 12,
1043
- padding: 12,
1044
- borderWidth: 1,
1045
- borderColor: night.color.border
1046
- },
1047
- sectionHeader: {
1048
- flexDirection: "row",
1049
- justifyContent: "space-between",
1050
- alignItems: "center",
1051
- marginBottom: 10,
1052
- paddingBottom: 6,
1053
- borderBottomWidth: 1,
1054
- borderBottomColor: night.color.separator
1055
- },
1056
- sectionHeaderLeft: {
1057
- flexDirection: "row",
1058
- alignItems: "center",
1059
- gap: 8
1060
- },
1061
- sectionTitle: {
1062
- fontSize: 12,
1063
- fontWeight: "600",
1064
- color: night.color.textSecondary,
1065
- letterSpacing: 0.4,
1066
- textTransform: "uppercase"
1067
- },
1068
- countBadge: {
1069
- backgroundColor: night.color.surfaceElevated,
1070
- paddingHorizontal: 8,
1071
- paddingVertical: 3,
1072
- borderRadius: 6,
1073
- borderWidth: 1,
1074
- borderColor: night.color.border
1075
- },
1076
- countText: {
1077
- fontSize: 11,
1078
- fontWeight: "600",
1079
- color: night.color.text,
1080
- fontFamily: monoFont
1081
- },
1082
- // Empty state
1083
- emptyState: {
1084
- alignItems: "center",
1085
- justifyContent: "center",
1086
- paddingVertical: 48
1087
- },
1088
- emptyTitle: {
1089
- fontSize: 16,
1090
- fontWeight: "600",
1091
- color: night.color.text,
1092
- marginTop: 12,
1093
- marginBottom: 8
1094
- },
1095
- emptySubtitle: {
1096
- fontSize: 13,
1097
- color: night.color.textSecondary,
1098
- textAlign: "center"
1099
- },
1100
- // Empty MMKV state
1101
- emptyMMKVState: {
1102
- backgroundColor: night.color.surface,
1103
- borderRadius: 12,
1104
- padding: 20,
1105
- borderWidth: 1,
1106
- borderColor: night.color.border,
1107
- marginTop: 8,
1108
- alignItems: "center"
1109
- },
1110
- emptyMMKVTitle: {
1111
- fontSize: 14,
1112
- fontWeight: "600",
1113
- color: night.color.text,
1114
- marginBottom: 6
1115
- },
1116
- emptyMMKVSubtitle: {
1117
- fontSize: 12,
1118
- color: night.color.textSecondary,
1119
- textAlign: "center",
1120
- marginBottom: 12
1121
- },
1122
- emptyMMKVCode: {
1123
- fontSize: 10,
1124
- fontFamily: monoFont,
1125
- color: night.color.accent,
1126
- backgroundColor: night.color.bg,
1127
- padding: 12,
1128
- borderRadius: 8,
1129
- borderWidth: 1,
1130
- borderColor: night.color.border,
1131
- alignSelf: "stretch"
1132
- },
1133
- // Instance Navbar styles
1134
- instanceNavbar: {
1135
- marginTop: 12,
1136
- marginBottom: 8,
1137
- backgroundColor: night.color.surface,
1138
- borderRadius: 10,
1139
- borderWidth: 1,
1140
- borderColor: night.color.border,
1141
- padding: 10,
1142
- shadowColor: '#000',
1143
- shadowOpacity: 0.08,
1144
- shadowRadius: 6,
1145
- shadowOffset: {
1146
- width: 0,
1147
- height: 2
1148
- }
1149
- },
1150
- instanceNavbarHeader: {
1151
- flexDirection: 'row',
1152
- alignItems: 'center',
1153
- justifyContent: 'space-between',
1154
- marginBottom: 8,
1155
- paddingLeft: 2
1156
- },
1157
- instanceNavbarLabel: {
1158
- fontSize: 9,
1159
- fontWeight: '700',
1160
- color: night.color.textTertiary,
1161
- textTransform: "uppercase",
1162
- letterSpacing: 1
1163
- },
1164
- instanceNavbarScroll: {
1165
- gap: 8,
1166
- paddingRight: 8
1167
- },
1168
- instanceNavbarButton: {
1169
- flexDirection: 'row',
1170
- alignItems: 'center',
1171
- gap: 6,
1172
- paddingHorizontal: 12,
1173
- paddingVertical: 8,
1174
- backgroundColor: night.color.surfaceElevated,
1175
- borderRadius: 8,
1176
- borderWidth: 1,
1177
- borderColor: night.color.border,
1178
- minWidth: 90
1179
- },
1180
- instanceNavbarButtonActive: {
1181
- borderWidth: 1.5,
1182
- shadowColor: '#000',
1183
- shadowOpacity: 0.1,
1184
- shadowRadius: 4,
1185
- shadowOffset: {
1186
- width: 0,
1187
- height: 1
1188
- }
1189
- },
1190
- instanceNavbarButtonText: {
1191
- fontSize: 11,
1192
- fontWeight: '600',
1193
- color: night.color.textSecondary,
1194
- fontFamily: monoFont,
1195
- flex: 1
1196
- },
1197
- instanceNavbarButtonTextActive: {
1198
- fontWeight: '700',
1199
- color: night.color.text
1200
- },
1201
- instanceNavbarBadge: {
1202
- backgroundColor: night.color.bg,
1203
- paddingHorizontal: 6,
1204
- paddingVertical: 2,
1205
- borderRadius: 6,
1206
- minWidth: 24,
1207
- alignItems: 'center'
1208
- },
1209
- instanceNavbarBadgeText: {
1210
- fontSize: 10,
1211
- fontWeight: '700',
1212
- color: night.color.textSecondary,
1213
- fontFamily: monoFont
1214
- },
1215
- clearSearchButton: {
1216
- marginTop: 16,
1217
- paddingHorizontal: 16,
1218
- paddingVertical: 8,
1219
- backgroundColor: night.color.surfaceElevated,
1220
- borderRadius: 6,
1221
- borderWidth: 1,
1222
- borderColor: night.color.border
1223
- },
1224
- clearSearchText: {
1225
- fontSize: 13,
1226
- color: night.color.text,
1227
- fontWeight: "500"
1228
- },
1229
- // Free tier limit banner
1230
- limitBanner: {
1231
- backgroundColor: night.color.surface,
1232
- borderRadius: 10,
1233
- borderWidth: 1,
1234
- borderColor: night.color.accentBorderStrong,
1235
- padding: 12,
1236
- marginTop: 12
1237
- },
1238
- limitBannerContent: {
1239
- flexDirection: "row",
1240
- alignItems: "center",
1241
- justifyContent: "space-between",
1242
- marginBottom: 4
1243
- },
1244
- limitBannerLeft: {
1245
- flexDirection: "row",
1246
- alignItems: "center",
1247
- gap: 8
1248
- },
1249
- limitBannerText: {
1250
- color: night.color.text,
1251
- fontSize: 13,
1252
- fontWeight: "600"
1253
- },
1254
- limitBannerSubtext: {
1255
- color: night.color.textTertiary,
1256
- fontSize: 11
1257
- }
1258
- });
3
+ const storage = createMMKV();
4
+ registerMMKVInstance('mmkv.default', storage);`})]}),C==="mmkv"&&y.length>0&&d(f,{style:n.instanceNavbar,children:[d(f,{style:n.instanceNavbarHeader,children:[a(m,{style:n.instanceNavbarLabel,children:"INSTANCES"}),y.length>Ce&&a(ze,{style:{transform:[{scale:.85}]}})]}),d(Re,{horizontal:true,showsHorizontalScrollIndicator:false,contentContainerStyle:n.instanceNavbarScroll,children:[(M||y.length===1)&&d(ae,{onPress:()=>Te(null),testID:"buoy-storage-instance-all",accessibilityRole:"button",accessibilityLabel:`All ${wt.mmkv} instances`,style:[n.instanceNavbarButton,g===null&&n.instanceNavbarButtonActive],children:[a(Fe,{size:12,color:g===null?s.color.text:s.color.textSecondary}),a(m,{style:[n.instanceNavbarButtonText,g===null&&n.instanceNavbarButtonTextActive],children:"All"}),a(f,{style:n.instanceNavbarBadge,children:a(m,{style:n.instanceNavbarBadgeText,children:y.reduce((e,t)=>e+t.keyCount,0)})})]}),y.map((e,t)=>{const r=t>=Ce;return d(ae,{testID:`buoy-storage-instance-${e.id}`,accessibilityRole:"button",accessibilityLabel:`MMKV instance ${e.id}`,onPress:()=>{if(r){q(true)}else{Te(e.id)}},style:[n.instanceNavbarButton,e.id===g&&n.instanceNavbarButtonActive,{borderColor:r?s.color.border:L(e.id)+"40",backgroundColor:e.id===g?L(e.id)+"20":s.color.surfaceElevated,opacity:r?.5:1}],children:[a(Fe,{size:12,color:e.id===g?L(e.id):s.color.textSecondary}),a(m,{style:[n.instanceNavbarButtonText,e.id===g&&{color:L(e.id),fontWeight:"700"}],children:e.id}),a(f,{style:[n.instanceNavbarBadge,e.id===g&&{backgroundColor:L(e.id)+"30"}],children:a(m,{style:[n.instanceNavbarBadgeText,e.id===g&&{color:L(e.id)}],children:e.keyCount})})]},e.id)})]})]})]});const bt=a(Ft,{copyValue:ut,mmkvInstances:y.map(e=>({id:e.id,instance:e.instance})),activeStorageType:C,onClearComplete:k,isSelectMode:te,onToggleSelectMode:at,selectedCount:U.size});const Ct=te?a(Wt,{selectedKeys:yt,mmkvInstances:y.map(e=>({id:e.id,instance:e.instance})),onDeleteComplete:ct,onHideKeys:ne,onSelectAll:mt,onClearSelection:ft,totalVisibleKeys:B.length}):null;const vt=gt?d(ae,{style:n.limitBanner,onPress:()=>q(true),activeOpacity:.8,testID:"buoy-storage-locked-banner",accessibilityRole:"button",children:[d(f,{style:n.limitBannerContent,children:[d(f,{style:n.limitBannerLeft,children:[a(Mt,{size:16,color:s.color.accent}),a(m,{style:n.limitBannerText,children:w?`${Y} matching ${Y===1?"key":"keys"} in locked storage`:`${Y} keys locked`})]}),a(ze,{})]}),a(m,{style:n.limitBannerSubtext,children:w?"Upgrade to Pro to search your full storage":"Upgrade to Pro to unlock full storage access"})]}):null;const Tt=(()=>{const e=c.some(r=>!p(r.key));let t;if(w||!e){t=Bt({subject:"keys",verb:"registered",search:w,total:e?1:0,waiting:{otto:"empty",title:"No storage keys",message:"Nothing is registered for this tool to read."}});if(w&&e){t={...t,message:"No keys match. Clear the search to see them."}}}else if(T==="missing"){t={otto:"thumbsup",title:"No missing keys",message:"All required keys are present"}}else if(T==="issues"){t={otto:"cool",title:"No issues found",message:"All storage keys are correctly configured"}}else{t={otto:"broken",title:"No valid keys found",message:"All keys have issues or are missing"}}return a(xt,{...t,testID:"buoy-storage-keys-empty"})})();const kt=a(Kt,{visible:Q,onClose:()=>q(false),featureName:"Storage Keys"});return d(f,{style:n.root,children:[we>0?a(Dt,{onDone:()=>Ie(0)},we):null,d(Re,{testID:"buoy-storage-scroll",ref:Je,onScroll:e=>{Xe.current=e.nativeEvent.contentOffset.y},scrollEventThrottle:32,style:n.scrollContainer,contentContainerStyle:n.container,showsVerticalScrollIndicator:false,children:[a(f,{style:n.backgroundGrid}),a(Vt,{stats:D,tabStats:x,healthPercentage:F,healthStatus:ht,healthColor:pt,activeFilter:T,onFilterChange:Ue,activeStorageType:C,onStorageTypeChange:Ye,showStatus:!ee}),St,v.length>0?d(f,{style:n.keysSection,children:[d(f,{style:n.sectionHeader,children:[d(f,{style:n.sectionHeaderLeft,children:[d(m,{style:n.sectionTitle,children:[T==="all"?"KEYS":T==="missing"?"MISSING KEYS":"ISSUES TO FIX",C!=="all"&&` (${C.toUpperCase()})`]}),a(f,{style:n.countBadge,children:a(m,{style:n.countText,children:B.length})})]}),bt]}),Ct,a(Nt,{title:"",count:-1,keys:B,emptyMessage:"",isSelectMode:te,selectedKeys:U,onSelectionChange:lt,eventCountByKey:ge,onViewHistory:Z,onHideKey:e=>ne([e]),onToggleKey:dt,onDeleteKey:Ne,editBlockedFor:qe,pinnedKeys:_,onTogglePin:he,onRowRef:Ze,onOpenKey:He}),vt]}):Tt,a(m,{style:n.techFooter,children:"ASYNC STORAGE | MMKV | SECURE STORAGE BACKENDS"}),kt]})]})}const n=It.create({root:{flex:1,minHeight:0},editorOverlay:{...Ae,zIndex:10},scrollContainer:{flex:1,backgroundColor:"transparent"},container:{padding:12,paddingBottom:120},backgroundGrid:{...Ae,opacity:.006,backgroundColor:E.info},techFooter:{fontSize:8,color:s.color.textTertiary,fontFamily:W,textAlign:"center",marginTop:20,letterSpacing:1,opacity:.5},keysSection:{marginTop:8,backgroundColor:s.color.bg,borderRadius:12,padding:12,borderWidth:1,borderColor:s.color.border},sectionHeader:{flexDirection:"row",justifyContent:"space-between",alignItems:"center",marginBottom:10,paddingBottom:6,borderBottomWidth:1,borderBottomColor:s.color.separator},sectionHeaderLeft:{flexDirection:"row",alignItems:"center",gap:8},sectionTitle:{fontSize:12,fontWeight:"600",color:s.color.textSecondary,letterSpacing:.4,textTransform:"uppercase"},countBadge:{backgroundColor:s.color.surfaceElevated,paddingHorizontal:8,paddingVertical:3,borderRadius:6,borderWidth:1,borderColor:s.color.border},countText:{fontSize:11,fontWeight:"600",color:s.color.text,fontFamily:W},emptyState:{alignItems:"center",justifyContent:"center",paddingVertical:48},emptyTitle:{fontSize:16,fontWeight:"600",color:s.color.text,marginTop:12,marginBottom:8},emptySubtitle:{fontSize:13,color:s.color.textSecondary,textAlign:"center"},emptyMMKVState:{backgroundColor:s.color.surface,borderRadius:12,padding:20,borderWidth:1,borderColor:s.color.border,marginTop:8,alignItems:"center"},emptyMMKVTitle:{fontSize:14,fontWeight:"600",color:s.color.text,marginBottom:6},emptyMMKVSubtitle:{fontSize:12,color:s.color.textSecondary,textAlign:"center",marginBottom:12},emptyMMKVCode:{fontSize:10,fontFamily:W,color:s.color.accent,backgroundColor:s.color.bg,padding:12,borderRadius:8,borderWidth:1,borderColor:s.color.border,alignSelf:"stretch"},instanceNavbar:{marginTop:12,marginBottom:8,backgroundColor:s.color.surface,borderRadius:10,borderWidth:1,borderColor:s.color.border,padding:10,shadowColor:"#000",shadowOpacity:.08,shadowRadius:6,shadowOffset:{width:0,height:2}},instanceNavbarHeader:{flexDirection:"row",alignItems:"center",justifyContent:"space-between",marginBottom:8,paddingLeft:2},instanceNavbarLabel:{fontSize:9,fontWeight:"700",color:s.color.textTertiary,textTransform:"uppercase",letterSpacing:1},instanceNavbarScroll:{gap:8,paddingRight:8},instanceNavbarButton:{flexDirection:"row",alignItems:"center",gap:6,paddingHorizontal:12,paddingVertical:8,backgroundColor:s.color.surfaceElevated,borderRadius:8,borderWidth:1,borderColor:s.color.border,minWidth:90},instanceNavbarButtonActive:{borderWidth:1.5,shadowColor:"#000",shadowOpacity:.1,shadowRadius:4,shadowOffset:{width:0,height:1}},instanceNavbarButtonText:{fontSize:11,fontWeight:"600",color:s.color.textSecondary,fontFamily:W,flex:1},instanceNavbarButtonTextActive:{fontWeight:"700",color:s.color.text},instanceNavbarBadge:{backgroundColor:s.color.bg,paddingHorizontal:6,paddingVertical:2,borderRadius:6,minWidth:24,alignItems:"center"},instanceNavbarBadgeText:{fontSize:10,fontWeight:"700",color:s.color.textSecondary,fontFamily:W},clearSearchButton:{marginTop:16,paddingHorizontal:16,paddingVertical:8,backgroundColor:s.color.surfaceElevated,borderRadius:6,borderWidth:1,borderColor:s.color.border},clearSearchText:{fontSize:13,color:s.color.text,fontWeight:"500"},limitBanner:{backgroundColor:s.color.surface,borderRadius:10,borderWidth:1,borderColor:s.color.accentBorderStrong,padding:12,marginTop:12},limitBannerContent:{flexDirection:"row",alignItems:"center",justifyContent:"space-between",marginBottom:4},limitBannerLeft:{flexDirection:"row",alignItems:"center",gap:8},limitBannerText:{color:s.color.text,fontSize:13,fontWeight:"600"},limitBannerSubtext:{color:s.color.textTertiary,fontSize:11}});