@buoy-gg/storage 7.0.40 → 7.0.42

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