@oxy.so/services 11.2.0 → 11.4.0

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 (141) hide show
  1. package/lib/commonjs/index.js +28 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/components/AccountInvitationAcceptance.js +97 -0
  4. package/lib/commonjs/ui/components/AccountInvitationAcceptance.js.map +1 -0
  5. package/lib/commonjs/ui/components/DisplayCurrencySelector.js +49 -0
  6. package/lib/commonjs/ui/components/DisplayCurrencySelector.js.map +1 -0
  7. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js +208 -0
  8. package/lib/commonjs/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  9. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js +77 -0
  10. package/lib/commonjs/ui/components/organizations/OrganizationClaims.js.map +1 -0
  11. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js +218 -0
  12. package/lib/commonjs/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  13. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js +147 -0
  14. package/lib/commonjs/ui/components/organizations/OrganizationSettings.js.map +1 -0
  15. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js +34 -0
  16. package/lib/commonjs/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  17. package/lib/commonjs/ui/hooks/useDisplayCurrency.js +77 -0
  18. package/lib/commonjs/ui/hooks/useDisplayCurrency.js.map +1 -0
  19. package/lib/commonjs/ui/hooks/useDisplayPrice.js +59 -0
  20. package/lib/commonjs/ui/hooks/useDisplayPrice.js.map +1 -0
  21. package/lib/commonjs/ui/screens/AccountMembersScreen.js +63 -20
  22. package/lib/commonjs/ui/screens/AccountMembersScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +14 -5
  24. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/CreateAccountScreen.js +148 -115
  26. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/ManageAccountScreen.js +13 -1
  28. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/PreferencesScreen.js +4 -3
  30. package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
  31. package/lib/module/index.js +4 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/ui/components/AccountInvitationAcceptance.js +94 -0
  34. package/lib/module/ui/components/AccountInvitationAcceptance.js.map +1 -0
  35. package/lib/module/ui/components/DisplayCurrencySelector.js +44 -0
  36. package/lib/module/ui/components/DisplayCurrencySelector.js.map +1 -0
  37. package/lib/module/ui/components/organizations/OrganizationClaimReview.js +205 -0
  38. package/lib/module/ui/components/organizations/OrganizationClaimReview.js.map +1 -0
  39. package/lib/module/ui/components/organizations/OrganizationClaims.js +73 -0
  40. package/lib/module/ui/components/organizations/OrganizationClaims.js.map +1 -0
  41. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js +215 -0
  42. package/lib/module/ui/components/organizations/OrganizationDirectorySearch.js.map +1 -0
  43. package/lib/module/ui/components/organizations/OrganizationSettings.js +143 -0
  44. package/lib/module/ui/components/organizations/OrganizationSettings.js.map +1 -0
  45. package/lib/module/ui/components/organizations/OrganizationTypePicker.js +30 -0
  46. package/lib/module/ui/components/organizations/OrganizationTypePicker.js.map +1 -0
  47. package/lib/module/ui/hooks/useDisplayCurrency.js +72 -0
  48. package/lib/module/ui/hooks/useDisplayCurrency.js.map +1 -0
  49. package/lib/module/ui/hooks/useDisplayPrice.js +56 -0
  50. package/lib/module/ui/hooks/useDisplayPrice.js.map +1 -0
  51. package/lib/module/ui/screens/AccountMembersScreen.js +63 -20
  52. package/lib/module/ui/screens/AccountMembersScreen.js.map +1 -1
  53. package/lib/module/ui/screens/AccountSettingsScreen.js +14 -5
  54. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  55. package/lib/module/ui/screens/CreateAccountScreen.js +149 -115
  56. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  57. package/lib/module/ui/screens/ManageAccountScreen.js +13 -1
  58. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  59. package/lib/module/ui/screens/PreferencesScreen.js +4 -3
  60. package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
  61. package/lib/typescript/commonjs/index.d.ts +4 -0
  62. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts +5 -0
  64. package/lib/typescript/commonjs/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
  65. package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts +3 -0
  66. package/lib/typescript/commonjs/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  68. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  70. package/lib/typescript/commonjs/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  72. package/lib/typescript/commonjs/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  74. package/lib/typescript/commonjs/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  76. package/lib/typescript/commonjs/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
  78. package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
  80. package/lib/typescript/commonjs/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts +11 -0
  82. package/lib/typescript/commonjs/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts +6 -0
  84. package/lib/typescript/commonjs/ui/hooks/useDisplayPrice.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  86. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  87. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts +1 -1
  88. package/lib/typescript/commonjs/ui/screens/AccountMembersScreen.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/screens/PreferencesScreen.d.ts.map +1 -1
  93. package/lib/typescript/module/index.d.ts +4 -0
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts +5 -0
  96. package/lib/typescript/module/ui/components/AccountInvitationAcceptance.d.ts.map +1 -0
  97. package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts +3 -0
  98. package/lib/typescript/module/ui/components/DisplayCurrencySelector.d.ts.map +1 -0
  99. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts +3 -0
  100. package/lib/typescript/module/ui/components/organizations/OrganizationClaimReview.d.ts.map +1 -0
  101. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts +2 -0
  102. package/lib/typescript/module/ui/components/organizations/OrganizationClaims.d.ts.map +1 -0
  103. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts +6 -0
  104. package/lib/typescript/module/ui/components/organizations/OrganizationDirectorySearch.d.ts.map +1 -0
  105. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts +5 -0
  106. package/lib/typescript/module/ui/components/organizations/OrganizationSettings.d.ts.map +1 -0
  107. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts +7 -0
  108. package/lib/typescript/module/ui/components/organizations/OrganizationTypePicker.d.ts.map +1 -0
  109. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +10 -3
  110. package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts +4 -3
  112. package/lib/typescript/module/ui/hooks/queries/usePersonalPlans.d.ts.map +1 -1
  113. package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts +11 -0
  114. package/lib/typescript/module/ui/hooks/useDisplayCurrency.d.ts.map +1 -0
  115. package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts +6 -0
  116. package/lib/typescript/module/ui/hooks/useDisplayPrice.d.ts.map +1 -0
  117. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  118. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  119. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts +1 -1
  120. package/lib/typescript/module/ui/screens/AccountMembersScreen.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  122. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
  125. package/package.json +9 -8
  126. package/src/index.ts +5 -0
  127. package/src/ui/components/AccountInvitationAcceptance.tsx +90 -0
  128. package/src/ui/components/DisplayCurrencySelector.tsx +38 -0
  129. package/src/ui/components/__tests__/AccountInvitationAcceptance.test.tsx +61 -0
  130. package/src/ui/components/organizations/OrganizationClaimReview.tsx +218 -0
  131. package/src/ui/components/organizations/OrganizationClaims.tsx +67 -0
  132. package/src/ui/components/organizations/OrganizationDirectorySearch.tsx +228 -0
  133. package/src/ui/components/organizations/OrganizationSettings.tsx +131 -0
  134. package/src/ui/components/organizations/OrganizationTypePicker.tsx +31 -0
  135. package/src/ui/hooks/useDisplayCurrency.ts +64 -0
  136. package/src/ui/hooks/useDisplayPrice.ts +40 -0
  137. package/src/ui/screens/AccountMembersScreen.tsx +71 -27
  138. package/src/ui/screens/AccountSettingsScreen.tsx +22 -4
  139. package/src/ui/screens/CreateAccountScreen.tsx +177 -124
  140. package/src/ui/screens/ManageAccountScreen.tsx +18 -0
  141. package/src/ui/screens/PreferencesScreen.tsx +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"usePersonalPlans.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/hooks/queries/usePersonalPlans.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;aA0B4xO,CAAC;;;;;;;;;UAnB5zO;AAED,kFAAkF;AAClF,wBAAgB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;YAe3C"}
1
+ {"version":3,"file":"usePersonalPlans.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/hooks/queries/usePersonalPlans.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;aA0Bk1O,CAAC;;;;;;;;;UAnBl3O;AAED,kFAAkF;AAClF,wBAAgB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;YAe3C"}
@@ -0,0 +1,11 @@
1
+ import { type UserPreferences } from '@oxy.so/core';
2
+ export type DisplayCurrency = NonNullable<UserPreferences['displayCurrency']>;
3
+ export declare const DISPLAY_CURRENCIES: readonly DisplayCurrency[];
4
+ /** Account-scoped server preference. A late save can never replace another account. */
5
+ export declare function useDisplayCurrency(): {
6
+ currency: DisplayCurrency;
7
+ selectCurrency: (next: DisplayCurrency) => void;
8
+ isSaving: boolean;
9
+ error: Error | null;
10
+ };
11
+ //# sourceMappingURL=useDisplayCurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDisplayCurrency.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useDisplayCurrency.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAO1E,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,eAAe,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAC9E,eAAO,MAAM,kBAAkB,EAAE,SAAS,eAAe,EAA2B,CAAC;AAYrF,uFAAuF;AACvF,wBAAgB,kBAAkB;;2BA6BF,eAAe;;;EAW9C"}
@@ -0,0 +1,6 @@
1
+ /** Display conversion only. Pricing and checkout request bodies retain their source units. */
2
+ export declare function useDisplayPrice(): {
3
+ currency: import("./useDisplayCurrency.js").DisplayCurrency;
4
+ formatUsd: (minorUnits: number) => string;
5
+ };
6
+ //# sourceMappingURL=useDisplayPrice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDisplayPrice.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useDisplayPrice.ts"],"names":[],"mappings":"AAMA,8FAA8F;AAC9F,wBAAgB,eAAe;;4BAkBE,MAAM;EActC"}
@@ -1,4 +1,4 @@
1
1
  /** Glyphs are decorative: hidden from assistive technology (see `decorativeIconSet`). */
2
- declare const Ionicons: import("@expo/vector-icons/build/createIconSet").Icon<"push" | "at" | "code" | "link" | "search" | "ellipse" | "image" | "text" | "add" | "add-circle-outline" | "alert" | "alert-circle" | "alert-circle-outline" | "apps" | "archive" | "arrow-back" | "arrow-down" | "arrow-forward" | "arrow-up" | "briefcase-outline" | "bug" | "bulb" | "business" | "business-outline" | "calendar" | "calendar-outline" | "camera" | "camera-outline" | "card" | "card-outline" | "cart-outline" | "cash-outline" | "chatbubble" | "checkmark" | "checkmark-circle" | "chevron-back" | "chevron-down" | "chevron-forward" | "chevron-up" | "close" | "close-circle" | "cloud-offline-outline" | "cloud-outline" | "cloud-upload" | "cloud-upload-outline" | "cog" | "cog-outline" | "construct-outline" | "copy-outline" | "cube-outline" | "desktop" | "document" | "document-outline" | "document-text" | "documents" | "download" | "download-outline" | "earth" | "ellipse-outline" | "ellipsis-horizontal" | "eye" | "eye-off-outline" | "filter-outline" | "flame" | "flash" | "folder" | "folder-open-outline" | "folder-outline" | "footsteps" | "gift" | "gift-outline" | "globe-outline" | "grid" | "hand-left" | "hardware-chip-outline" | "heart" | "heart-outline" | "help-circle" | "help-circle-outline" | "hourglass-outline" | "image-outline" | "images" | "images-outline" | "infinite" | "information" | "information-circle" | "information-outline" | "key" | "leaf" | "library" | "link-outline" | "list" | "location" | "location-outline" | "lock-closed" | "log-out-outline" | "musical-notes" | "navigate" | "open" | "pencil-outline" | "people" | "people-outline" | "play" | "pricetag-outline" | "pulse" | "qr-code-outline" | "radio" | "receipt-outline" | "refresh-outline" | "remove" | "repeat-outline" | "resize" | "rocket" | "scale" | "school-outline" | "search-outline" | "server" | "settings" | "share-outline" | "shield" | "shield-checkmark" | "shield-checkmark-outline" | "star" | "swap-horizontal-outline" | "swap-vertical-outline" | "sync" | "time-outline" | "trash" | "trash-outline" | "trophy" | "trophy-outline" | "videocam" | "volume-off" | "wallet" | "wallet-outline" | "warning" | "watch" | "wifi", "OxyServicesIonicons">;
2
+ declare const Ionicons: import("@expo/vector-icons/build/createIconSet").Icon<"push" | "at" | "code" | "business" | "open" | "add" | "desktop" | "search" | "navigate" | "warning" | "text" | "scale" | "link" | "image" | "key" | "resize" | "ellipse" | "alert" | "radio" | "list" | "infinite" | "alert-circle" | "alert-circle-outline" | "apps" | "archive" | "arrow-down" | "arrow-up" | "briefcase-outline" | "bug" | "calendar" | "calendar-outline" | "camera" | "camera-outline" | "card" | "card-outline" | "cart-outline" | "chevron-down" | "chevron-up" | "close" | "close-circle" | "cloud-outline" | "cloud-upload" | "cloud-upload-outline" | "cog" | "cog-outline" | "cube-outline" | "download" | "download-outline" | "earth" | "ellipse-outline" | "eye" | "eye-off-outline" | "filter-outline" | "flash" | "folder" | "folder-open-outline" | "folder-outline" | "gift" | "gift-outline" | "grid" | "heart" | "heart-outline" | "help-circle" | "help-circle-outline" | "image-outline" | "information" | "information-outline" | "leaf" | "library" | "pencil-outline" | "play" | "pulse" | "receipt-outline" | "rocket" | "school-outline" | "server" | "share-outline" | "shield" | "star" | "sync" | "trophy" | "trophy-outline" | "volume-off" | "wallet" | "wallet-outline" | "watch" | "wifi" | "images" | "settings" | "add-circle-outline" | "arrow-back" | "arrow-forward" | "bulb" | "business-outline" | "cash-outline" | "chatbubble" | "checkmark" | "checkmark-circle" | "chevron-back" | "chevron-forward" | "cloud-offline-outline" | "construct-outline" | "copy-outline" | "document" | "document-outline" | "document-text" | "documents" | "ellipsis-horizontal" | "flame" | "footsteps" | "globe-outline" | "hand-left" | "hardware-chip-outline" | "hourglass-outline" | "images-outline" | "information-circle" | "link-outline" | "location" | "location-outline" | "lock-closed" | "log-out-outline" | "musical-notes" | "people" | "people-outline" | "pricetag-outline" | "qr-code-outline" | "refresh-outline" | "remove" | "repeat-outline" | "search-outline" | "shield-checkmark" | "shield-checkmark-outline" | "swap-horizontal-outline" | "swap-vertical-outline" | "time-outline" | "trash" | "trash-outline" | "videocam", "OxyServicesIonicons">;
3
3
  export default Ionicons;
4
4
  //# sourceMappingURL=Ionicons.d.ts.map
@@ -1,4 +1,4 @@
1
1
  /** Glyphs are decorative: hidden from assistive technology (see `decorativeIconSet`). */
2
- declare const MaterialCommunityIcons: import("@expo/vector-icons/build/createIconSet").Icon<"function" | "devices" | "sort" | "at" | "label" | "link" | "ellipse" | "image" | "switch" | "text" | "alert" | "alert-circle" | "alert-circle-outline" | "apps" | "archive" | "arrow-down" | "arrow-up" | "briefcase-outline" | "bug" | "calendar" | "calendar-outline" | "camera" | "camera-outline" | "card" | "card-outline" | "cart-outline" | "chevron-down" | "chevron-up" | "close" | "close-circle" | "cloud-outline" | "cloud-upload" | "cloud-upload-outline" | "cog" | "cog-outline" | "cube-outline" | "download" | "download-outline" | "earth" | "ellipse-outline" | "eye" | "eye-off-outline" | "filter-outline" | "flash" | "folder" | "folder-open-outline" | "folder-outline" | "gift" | "gift-outline" | "grid" | "heart" | "heart-outline" | "help-circle" | "help-circle-outline" | "image-outline" | "information" | "information-outline" | "key" | "leaf" | "library" | "pencil-outline" | "play" | "pulse" | "radio" | "receipt-outline" | "resize" | "rocket" | "scale" | "school-outline" | "server" | "share-outline" | "shield" | "star" | "sync" | "trophy" | "trophy-outline" | "volume-off" | "wallet" | "wallet-outline" | "watch" | "wifi" | "solid" | "opacity" | "wrap" | "account" | "account-circle" | "account-cog-outline" | "account-eye-outline" | "account-group" | "account-multiple" | "account-multiple-outline" | "account-outline" | "account-plus" | "account-search" | "account-switch" | "android" | "archive-arrow-down" | "arrow-left" | "bell" | "bell-outline" | "bio" | "bullhorn" | "cake-variant" | "cake-variant-outline" | "cancel" | "cellphone" | "cellphone-basic" | "cellphone-message" | "cellphone-screenshot" | "chart-line" | "check-all" | "check-circle" | "chip" | "circle-outline" | "clock" | "clock-outline" | "code-tags" | "contain" | "content-copy" | "controller" | "cookie" | "credit-card" | "database-outline" | "delete" | "delete-outline" | "dots-horizontal" | "email" | "email-outline" | "file" | "file-document" | "file-document-edit-outline" | "file-document-outline" | "file-excel-outline" | "file-outline" | "file-pdf-box" | "folder-image" | "folder-zip-outline" | "ghost" | "history" | "image-multiple-outline" | "link-variant" | "loading" | "lock-outline" | "logout" | "magnify" | "map-marker" | "map-marker-multiple" | "map-marker-outline" | "message" | "message-outline" | "minus-circle" | "motion" | "music-note-outline" | "network" | "null" | "numeric" | "open-in-app" | "openid" | "palette" | "palette-outline" | "pan" | "phone" | "phone-outline" | "plus" | "plus-circle" | "post" | "presentation" | "react" | "read" | "security" | "shield-account-outline" | "shield-check" | "shield-search" | "signature" | "sort-alphabetical-variant" | "sort-numeric-variant" | "store" | "tag" | "tag-outline" | "text-box-outline" | "theme-light-dark" | "transfer" | "translate" | "trash-can-outline" | "tray-arrow-down" | "update" | "video-outline" | "web" | "wifi-off" | "wrench" | "xml", "OxyServicesMaterialCommunityIcons">;
2
+ declare const MaterialCommunityIcons: import("@expo/vector-icons/build/createIconSet").Icon<"function" | "devices" | "sort" | "at" | "security" | "account" | "message" | "network" | "email" | "bio" | "phone" | "web" | "read" | "android" | "logout" | "store" | "label" | "pan" | "text" | "solid" | "scale" | "link" | "loading" | "image" | "key" | "resize" | "ellipse" | "switch" | "opacity" | "wrap" | "alert" | "radio" | "contain" | "account-circle" | "account-cog-outline" | "account-eye-outline" | "account-group" | "account-multiple" | "account-multiple-outline" | "account-outline" | "account-plus" | "account-search" | "account-switch" | "alert-circle" | "alert-circle-outline" | "apps" | "archive" | "archive-arrow-down" | "arrow-down" | "arrow-left" | "arrow-up" | "bell" | "bell-outline" | "briefcase-outline" | "bug" | "bullhorn" | "cake-variant" | "cake-variant-outline" | "calendar" | "calendar-outline" | "camera" | "camera-outline" | "cancel" | "card" | "card-outline" | "cart-outline" | "cellphone" | "cellphone-basic" | "cellphone-message" | "cellphone-screenshot" | "chart-line" | "check-all" | "check-circle" | "chevron-down" | "chevron-up" | "chip" | "circle-outline" | "clock" | "clock-outline" | "close" | "close-circle" | "cloud-outline" | "cloud-upload" | "cloud-upload-outline" | "code-tags" | "cog" | "cog-outline" | "content-copy" | "controller" | "cookie" | "credit-card" | "cube-outline" | "database-outline" | "delete" | "delete-outline" | "dots-horizontal" | "download" | "download-outline" | "earth" | "ellipse-outline" | "email-outline" | "eye" | "eye-off-outline" | "file" | "file-document" | "file-document-edit-outline" | "file-document-outline" | "file-excel-outline" | "file-outline" | "file-pdf-box" | "filter-outline" | "flash" | "folder" | "folder-image" | "folder-open-outline" | "folder-outline" | "folder-zip-outline" | "ghost" | "gift" | "gift-outline" | "grid" | "heart" | "heart-outline" | "help-circle" | "help-circle-outline" | "history" | "image-multiple-outline" | "image-outline" | "information" | "information-outline" | "leaf" | "library" | "link-variant" | "lock-outline" | "magnify" | "map-marker" | "map-marker-multiple" | "map-marker-outline" | "message-outline" | "minus-circle" | "motion" | "music-note-outline" | "null" | "numeric" | "open-in-app" | "openid" | "palette" | "palette-outline" | "pencil-outline" | "phone-outline" | "play" | "plus" | "plus-circle" | "post" | "presentation" | "pulse" | "react" | "receipt-outline" | "rocket" | "school-outline" | "server" | "share-outline" | "shield" | "shield-account-outline" | "shield-check" | "shield-search" | "signature" | "sort-alphabetical-variant" | "sort-numeric-variant" | "star" | "sync" | "tag" | "tag-outline" | "text-box-outline" | "theme-light-dark" | "transfer" | "translate" | "trash-can-outline" | "tray-arrow-down" | "trophy" | "trophy-outline" | "update" | "video-outline" | "volume-off" | "wallet" | "wallet-outline" | "watch" | "wifi" | "wifi-off" | "wrench" | "xml", "OxyServicesMaterialCommunityIcons">;
3
3
  export default MaterialCommunityIcons;
4
4
  //# sourceMappingURL=MaterialCommunityIcons.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import type React from 'react';
2
2
  import type { BaseScreenProps } from '../types/navigation.js';
3
3
  /**
4
- * Manage the members of an account (invite by username/email, change roles,
4
+ * Manage the members of an account (invite by email, change roles,
5
5
  * remove, transfer ownership). Gated on the caller's `callerMembership`
6
6
  * permissions. Receives the target `accountId` via navigation props.
7
7
  */
@@ -1 +1 @@
1
- {"version":3,"file":"AccountMembersScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountMembersScreen.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAkB3D;;;;GAIG;AACH,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAscnD,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"AccountMembersScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountMembersScreen.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAa3D;;;;GAIG;AACH,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAufnD,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAkB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAa3D;;;;;;GAMG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAiQpD,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"AccountSettingsScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/AccountSettingsScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAa3D;;;;;;GAMG;AACH,QAAA,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAiRpD,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAsB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAsF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CAialD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"CreateAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/CreateAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA6B/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AAsF3D;;;;;GAKG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA+clD,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ManageAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ManageAccountScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAiB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AA+C3D;;;;;;;GAOG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA8mBlD,CAAC;AAYF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"ManageAccountScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/ManageAccountScreen.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAiB/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;AA+C3D;;;;;;;GAOG;AACH,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA8nBlD,CAAC;AAYF,eAAe,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"PreferencesScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/PreferencesScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AAOzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;;AAwL3D,wBAA6C"}
1
+ {"version":3,"file":"PreferencesScreen.d.ts","sourceRoot":"","sources":["../../../../../src/ui/screens/PreferencesScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AAQzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAqB,CAAC;;AAyL3D,wBAA6C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/services",
3
- "version": "11.2.0",
3
+ "version": "11.4.0",
4
4
  "description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -155,18 +155,19 @@
155
155
  },
156
156
  "dependencies": {
157
157
  "@expo/config-plugins": "57.0.9",
158
- "@oxy.so/contracts": "4.12.0",
159
- "color": "^4.2.3"
158
+ "@oxy.so/contracts": "4.15.0",
159
+ "color": "^5.0.3",
160
+ "@peable.to/sdk": "0.2.7-oxy-one.1"
160
161
  },
161
162
  "devDependencies": {
162
163
  "@commitlint/cli": "^21.2.1",
163
164
  "@commitlint/config-conventional": "^21.2.0",
164
165
  "@expo/vector-icons": "^15.0.3",
165
- "@oxy.so/core": "4.5.0",
166
+ "@oxy.so/core": "4.9.0",
166
167
  "@react-native-async-storage/async-storage": "^2.0.0",
167
168
  "@react-native-community/netinfo": "^12.0.1",
168
169
  "@react-native/babel-preset": "^0.86.2",
169
- "@release-it/conventional-changelog": "^10.0.6",
170
+ "@release-it/conventional-changelog": "^12.0.2",
170
171
  "@tanstack/query-async-storage-persister": "^5.101",
171
172
  "@tanstack/query-sync-storage-persister": "^5.101",
172
173
  "@tanstack/react-query": "^5.101",
@@ -177,7 +178,7 @@
177
178
  "@types/jest": "^30.0.0",
178
179
  "@types/node": "^20.19.43",
179
180
  "@types/react": "*",
180
- "@oxy.so/bloom": "6.2.1",
181
+ "@oxy.so/bloom": "^10.4.0",
181
182
  "copyfiles": "^2.4.1",
182
183
  "expo-clipboard": "~57.0.2",
183
184
  "expo-constants": "~57.0.5",
@@ -188,7 +189,7 @@
188
189
  "expo-linear-gradient": "~57.0.1",
189
190
  "expo-notifications": "~57.0.5",
190
191
  "expo-modules-core": "~57.0.17",
191
- "expo-secure-store": "~57.0.1",
192
+ "expo-secure-store": "~57.0.4",
192
193
  "expo-web-browser": "~57.0.1",
193
194
  "husky": "^4.3.8",
194
195
  "jest": "^30.5.1",
@@ -215,7 +216,7 @@
215
216
  "peerDependencies": {
216
217
  "@expo/vector-icons": "^15.0.3",
217
218
  "@oxy.so/bloom": ">=6.2.1 <6.3.0 || >=7.1.2 <7.2.0 || >=7.7.0 <7.8.0 || >=7.13.1 <7.34.0 || >=8.0.0 <8.2.0 || >=9.0.0 <9.1.0 || >=10.0.0",
218
- "@oxy.so/core": "4.5.0",
219
+ "@oxy.so/core": "4.9.0",
219
220
  "@react-native-async-storage/async-storage": "^2.0.0",
220
221
  "@react-native-community/netinfo": ">=11.4.1 <13.0.0",
221
222
  "@tanstack/query-async-storage-persister": "^5.101",
package/src/index.ts CHANGED
@@ -453,3 +453,8 @@ export {
453
453
  usePersonalPlans,
454
454
  usePersonalPlanSubscriptions,
455
455
  } from './ui/hooks/queries/usePersonalPlans';
456
+ export { AccountInvitationAcceptance } from './ui/components/AccountInvitationAcceptance';
457
+
458
+ export { DisplayCurrencySelector } from './ui/components/DisplayCurrencySelector';
459
+ export { useDisplayCurrency } from './ui/hooks/useDisplayCurrency';
460
+ export { useDisplayPrice } from './ui/hooks/useDisplayPrice';
@@ -0,0 +1,90 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import { Text } from '@oxy.so/bloom/typography';
4
+ import { Button } from '@oxy.so/bloom/button';
5
+ import type { AccountInvitationPreview } from '@oxy.so/core';
6
+ import { getNormalizedUserHandle } from '@oxy.so/core';
7
+ import { useOxy } from '../context/OxyContext';
8
+
9
+ /** The link never grants access on navigation, preview, sign-in or account switching. */
10
+ export function AccountInvitationAcceptance({ token }: { token: string }) {
11
+ return <InvitationContent key={token} token={token} />;
12
+ }
13
+
14
+ function InvitationContent({ token }: { token: string }) {
15
+ const { oxyServices, user, canUsePrivateApi, openAccountDialog } = useOxy();
16
+ const [preview, setPreview] = useState<AccountInvitationPreview | null>(null);
17
+ const [error, setError] = useState('');
18
+ const [busy, setBusy] = useState(false);
19
+ const [accepted, setAccepted] = useState(false);
20
+ useEffect(() => {
21
+ let alive = true;
22
+ setPreview(null);
23
+ setError('');
24
+ if (canUsePrivateApi && token) {
25
+ oxyServices.accounts.invitations
26
+ .preview(token)
27
+ .then((value) => {
28
+ if (alive) setPreview(value);
29
+ })
30
+ .catch((reason) => {
31
+ if (alive) setError(reason instanceof Error ? reason.message : 'Invitation unavailable');
32
+ });
33
+ }
34
+ return () => {
35
+ alive = false;
36
+ };
37
+ }, [oxyServices, canUsePrivateApi, token]);
38
+
39
+ const accept = async () => {
40
+ if (!user || !preview) return;
41
+ setBusy(true);
42
+ setError('');
43
+ try {
44
+ await oxyServices.accounts.invitations.accept(token, user.id);
45
+ setAccepted(true);
46
+ } catch (reason) {
47
+ setError(reason instanceof Error ? reason.message : 'Could not accept invitation');
48
+ } finally {
49
+ setBusy(false);
50
+ }
51
+ };
52
+ return (
53
+ <View className="gap-space-16 p-space-24">
54
+ <Text className="text-title font-semibold text-text">Account invitation</Text>
55
+ {canUsePrivateApi && token && !preview && !error ? (
56
+ <Text accessibilityRole="progressbar">Loading invitation…</Text>
57
+ ) : null}
58
+ {accepted ? (
59
+ <Text>Invitation accepted. You can now access this account.</Text>
60
+ ) : (
61
+ <>
62
+ {preview ? (
63
+ <Text>
64
+ Join {preview.account.displayName ?? preview.account.username} as{' '}
65
+ {preview.invitation.role}.
66
+ </Text>
67
+ ) : null}
68
+ <Text>
69
+ Selected account:{' '}
70
+ {user
71
+ ? `${user.name?.displayName ?? getNormalizedUserHandle(user) ?? user.id} (${user.email ?? user.id})`
72
+ : 'Not signed in'}
73
+ </Text>
74
+ <Text>
75
+ The selected account receives access. Its email can differ from the invitation address.
76
+ No access is granted until you confirm.
77
+ </Text>
78
+ <Button disabled={busy} onPress={() => openAccountDialog()}>
79
+ {user ? 'Change account' : 'Sign in or create account'}
80
+ </Button>
81
+ <Button disabled={busy || !user || !preview || user.kind !== 'personal'} onPress={accept}>
82
+ Confirm and accept
83
+ </Button>
84
+ </>
85
+ )}
86
+ {error ? <Text accessibilityRole="alert">{error}</Text> : null}
87
+ {!token ? <Text>This invitation link is incomplete.</Text> : null}
88
+ </View>
89
+ );
90
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import {
4
+ SegmentedControl,
5
+ SegmentedControlItem,
6
+ SegmentedControlItemText,
7
+ } from '@oxy.so/bloom/segmented-control';
8
+ import { Text } from '@oxy.so/bloom/typography';
9
+ import { DISPLAY_CURRENCIES, useDisplayCurrency } from '../hooks/useDisplayCurrency';
10
+ import { useI18n } from '../hooks/useI18n';
11
+
12
+ export function DisplayCurrencySelector() {
13
+ const { currency, selectCurrency, isSaving, error } = useDisplayCurrency();
14
+ const { t } = useI18n();
15
+ return (
16
+ <View>
17
+ <Text>{t('preferences.displayCurrency.label')}</Text>
18
+ <SegmentedControl
19
+ type="radio"
20
+ label={t('preferences.displayCurrency.label')}
21
+ value={currency}
22
+ onValueChange={(value) => {
23
+ const selected = DISPLAY_CURRENCIES.find((item) => item === value);
24
+ if (selected && !isSaving) selectCurrency(selected);
25
+ }}
26
+ >
27
+ {DISPLAY_CURRENCIES.map((value) => (
28
+ <SegmentedControlItem key={value} value={value} disabled={isSaving}>
29
+ <SegmentedControlItemText>{value}</SegmentedControlItemText>
30
+ </SegmentedControlItem>
31
+ ))}
32
+ </SegmentedControl>
33
+ {error && (
34
+ <Text accessibilityLiveRegion="polite">{t('preferences.displayCurrency.saveFailed')}</Text>
35
+ )}
36
+ </View>
37
+ );
38
+ }
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
3
+ import { AccountInvitationAcceptance } from '../AccountInvitationAcceptance';
4
+ const mockPreview = jest.fn();
5
+ const mockAccept = jest.fn();
6
+ const mockSwitch = jest.fn();
7
+ let mockUser: { id: string; kind: string; email: string; username: string } | null = null;
8
+ const mockServices = { accounts: { invitations: { preview: mockPreview, accept: mockAccept } } };
9
+ jest.mock('../../context/OxyContext', () => ({
10
+ useOxy: () => ({
11
+ oxyServices: mockServices,
12
+ user: mockUser,
13
+ canUsePrivateApi: !!mockUser,
14
+ openAccountDialog: mockSwitch,
15
+ }),
16
+ }));
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ mockUser = null;
20
+ mockPreview.mockResolvedValue({
21
+ invitation: { role: 'editor' },
22
+ account: { displayName: 'Community' },
23
+ });
24
+ mockAccept.mockResolvedValue({ success: true });
25
+ });
26
+ test('sign-in and switching never accept; confirmation names the displayed selected account', async () => {
27
+ const { rerender } = render(<AccountInvitationAcceptance token="secret" />);
28
+ fireEvent.click(screen.getByText('Sign in or create account'));
29
+ expect(mockSwitch).toHaveBeenCalled();
30
+ expect(mockAccept).not.toHaveBeenCalled();
31
+ mockUser = { id: 'first', kind: 'personal', username: 'first', email: 'different@example.com' };
32
+ rerender(<AccountInvitationAcceptance token="secret" />);
33
+ await screen.findByText('Join Community as editor.');
34
+ fireEvent.click(screen.getByText('Change account'));
35
+ expect(mockAccept).not.toHaveBeenCalled();
36
+ mockUser = { id: 'second', kind: 'personal', username: 'second', email: 'second@example.com' };
37
+ rerender(<AccountInvitationAcceptance token="secret" />);
38
+ expect(screen.getByText(/Selected account: second/)).toBeTruthy();
39
+ fireEvent.click(screen.getByText('Confirm and accept'));
40
+ await waitFor(() => expect(mockAccept).toHaveBeenCalledWith('secret', 'second'));
41
+ await screen.findByText('Invitation accepted. You can now access this account.');
42
+ });
43
+ test('a new link resets previous acceptance and does not auto-accept', async () => {
44
+ mockUser = { id: 'first', kind: 'personal', username: 'first', email: 'first@example.com' };
45
+ const { rerender } = render(<AccountInvitationAcceptance token="old" />);
46
+ await screen.findByText('Join Community as editor.');
47
+ fireEvent.click(screen.getByText('Confirm and accept'));
48
+ await screen.findByText('Invitation accepted. You can now access this account.');
49
+ rerender(<AccountInvitationAcceptance token="new" />);
50
+ await screen.findByText('Join Community as editor.');
51
+ expect(screen.queryByText('Invitation accepted. You can now access this account.')).toBeNull();
52
+ expect(mockAccept).toHaveBeenCalledTimes(1);
53
+ });
54
+ test('preview failure leaves confirmation disabled', async () => {
55
+ mockUser = { id: 'first', kind: 'personal', username: 'first', email: 'first@example.com' };
56
+ mockPreview.mockRejectedValue(new Error('Invitation expired'));
57
+ render(<AccountInvitationAcceptance token="expired" />);
58
+ await screen.findByText('Invitation expired');
59
+ fireEvent.click(screen.getByText('Confirm and accept'));
60
+ expect(mockAccept).not.toHaveBeenCalled();
61
+ });
@@ -0,0 +1,218 @@
1
+ import { useState } from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
+ import { isHttpRequestError } from '@oxy.so/core';
5
+ import {
6
+ organizationClaimReviewSchema,
7
+ type OrganizationClaimReview as OrganizationClaimReviewInput,
8
+ } from '@oxy.so/contracts';
9
+ import { Button } from '@oxy.so/bloom/button';
10
+ import { SettingsListGroup, SettingsListItem } from '@oxy.so/bloom/settings-list';
11
+ import { TextField, TextFieldInput } from '@oxy.so/bloom/text-field';
12
+ import { Text } from '@oxy.so/bloom/typography';
13
+ import { useOxy } from '../../context/OxyContext';
14
+ import { useI18n } from '../../hooks/useI18n';
15
+
16
+ /** The queue is requested only after an explicit action; the server rechecks staff authority. */
17
+ export function OrganizationClaimReview() {
18
+ const { t } = useI18n();
19
+ const { user, canUsePrivateApi } = useOxy();
20
+ const [open, setOpen] = useState(false);
21
+ return (
22
+ <SettingsListGroup>
23
+ <SettingsListItem
24
+ title={t('organizationDirectory.review.title')}
25
+ disabled={!canUsePrivateApi}
26
+ onPress={() => setOpen((value) => !value)}
27
+ />
28
+ {open && user?.kind === 'personal' && canUsePrivateApi ? <ReviewQueue key={user.id} /> : null}
29
+ </SettingsListGroup>
30
+ );
31
+ }
32
+ function ReviewQueue() {
33
+ const { t } = useI18n();
34
+ const { oxyServices, user, canUsePrivateApi } = useOxy();
35
+ const [selected, setSelected] = useState<string | null>(null);
36
+ const requests = useInfiniteQuery({
37
+ queryKey: ['oxy', 'organizations', 'review', user?.id, canUsePrivateApi],
38
+ queryFn: ({ pageParam }) =>
39
+ oxyServices.organizations.reviewClaims({
40
+ status: 'pending',
41
+ limit: 20,
42
+ ...(pageParam ? { cursor: pageParam } : {}),
43
+ }),
44
+ initialPageParam: undefined as string | undefined,
45
+ getNextPageParam: (page) => page.nextCursor ?? undefined,
46
+ enabled: canUsePrivateApi,
47
+ retry: false,
48
+ gcTime: 0,
49
+ });
50
+ const claims = requests.data?.pages.flatMap((page) => page.items) ?? [];
51
+ const ids = [...new Set(claims.map((claim) => claim.accountId))].sort();
52
+ const organizations = useQuery({
53
+ queryKey: ['oxy', 'organizations', 'review-organizations', ids],
54
+ queryFn: async () => {
55
+ const batches = [];
56
+ for (let offset = 0; offset < ids.length; offset += 100)
57
+ batches.push(oxyServices.organizations.batch({ ids: ids.slice(offset, offset + 100) }));
58
+ return (await Promise.all(batches)).flatMap((batch) => batch.items);
59
+ },
60
+ enabled: canUsePrivateApi && requests.isSuccess && ids.length > 0,
61
+ retry: false,
62
+ });
63
+ if (requests.isPending) return <ActivityIndicator />;
64
+ if (requests.isError)
65
+ return (
66
+ <View className="gap-space-16 p-space-16">
67
+ <Text accessibilityRole="alert">
68
+ {t(
69
+ isHttpRequestError(requests.error) && requests.error.status === 403
70
+ ? 'organizationDirectory.review.forbidden'
71
+ : 'organizationDirectory.requestError',
72
+ )}
73
+ </Text>
74
+ <Button onPress={() => requests.refetch()}>{t('organizationDirectory.retry')}</Button>
75
+ </View>
76
+ );
77
+ if (selected)
78
+ return <ReviewDetail claimId={selected} key={selected} onClose={() => setSelected(null)} />;
79
+ return (
80
+ <View className="gap-space-16 p-space-16">
81
+ {claims.length === 0 ? <Text>{t('organizationDirectory.review.empty')}</Text> : null}
82
+ {claims.map((claim) => (
83
+ <SettingsListItem
84
+ key={claim.id}
85
+ title={
86
+ organizations.data?.find((item) => item.requestedAccountId === claim.accountId)
87
+ ?.organization.name ?? t('organizationDirectory.view')
88
+ }
89
+ description={t('organizationDirectory.pending')}
90
+ onPress={() => setSelected(claim.id)}
91
+ />
92
+ ))}
93
+ {requests.hasNextPage ? (
94
+ <Button onPress={() => requests.fetchNextPage()} disabled={requests.isFetchingNextPage}>
95
+ {t('organizationDirectory.loadMore')}
96
+ </Button>
97
+ ) : null}
98
+ </View>
99
+ );
100
+ }
101
+ function ReviewDetail({ claimId, onClose }: { claimId: string; onClose: () => void }) {
102
+ const { t } = useI18n();
103
+ const { oxyServices, user, canUsePrivateApi } = useOxy();
104
+ const queryClient = useQueryClient();
105
+ const [evidence, setEvidence] = useState('');
106
+ const [authorizationRevoked, setAuthorizationRevoked] = useState(false);
107
+ const detail = useQuery({
108
+ queryKey: ['oxy', 'organizations', 'review-detail', user?.id, canUsePrivateApi, claimId],
109
+ queryFn: () => oxyServices.organizations.reviewClaimDetail(claimId),
110
+ enabled: canUsePrivateApi,
111
+ retry: false,
112
+ gcTime: 0,
113
+ });
114
+ const review = useMutation({
115
+ mutationFn: (decision: OrganizationClaimReviewInput['decision']) => {
116
+ if (
117
+ !canUsePrivateApi ||
118
+ authorizationRevoked ||
119
+ !detail.isSuccess ||
120
+ !detail.data ||
121
+ detail.data.claimantUserId === user?.id
122
+ )
123
+ throw new Error('Independent staff review is required');
124
+ return oxyServices.organizations.reviewClaim(
125
+ claimId,
126
+ organizationClaimReviewSchema.parse({ decision, evidenceReference: evidence }),
127
+ );
128
+ },
129
+ onError: (error) => {
130
+ if (isHttpRequestError(error) && error.status === 403) setAuthorizationRevoked(true);
131
+ },
132
+ onSuccess: async () => {
133
+ await queryClient.invalidateQueries({ queryKey: ['oxy', 'organizations'] });
134
+ await queryClient.invalidateQueries({ queryKey: ['accounts'] });
135
+ onClose();
136
+ },
137
+ });
138
+ const conflict = isHttpRequestError(review.error) && review.error.status === 409;
139
+ const forbidden = isHttpRequestError(review.error) && review.error.status === 403;
140
+ const selfReview = detail.data?.claimantUserId === user?.id;
141
+ return (
142
+ <View className="gap-space-16 p-space-16">
143
+ {detail.isPending ? <ActivityIndicator /> : null}
144
+ {detail.isError ? (
145
+ <>
146
+ <Text accessibilityRole="alert">
147
+ {t(
148
+ isHttpRequestError(detail.error) && detail.error.status === 403
149
+ ? 'organizationDirectory.review.forbidden'
150
+ : 'organizationDirectory.requestError',
151
+ )}
152
+ </Text>
153
+ <Button onPress={() => detail.refetch()}>{t('organizationDirectory.retry')}</Button>
154
+ </>
155
+ ) : null}
156
+ {authorizationRevoked ? (
157
+ <Text accessibilityRole="alert">{t('organizationDirectory.review.forbidden')}</Text>
158
+ ) : null}
159
+ {detail.isSuccess && canUsePrivateApi && !authorizationRevoked ? (
160
+ <>
161
+ <Text>{t('organizationDirectory.evidence')}</Text>
162
+ <Text selectable>{detail.data.evidenceReference}</Text>
163
+ <Text>{t('organizationDirectory.review.hint')}</Text>
164
+ {selfReview ? <Text>{t('organizationDirectory.review.independent')}</Text> : null}
165
+ <TextField>
166
+ <TextFieldInput
167
+ label={t('organizationDirectory.review.evidence')}
168
+ value={evidence}
169
+ onChangeText={setEvidence}
170
+ maxLength={1000}
171
+ />
172
+ </TextField>
173
+ {review.isError ? (
174
+ <Text accessibilityRole="alert">
175
+ {t(
176
+ conflict
177
+ ? 'organizationDirectory.review.conflict'
178
+ : forbidden
179
+ ? 'organizationDirectory.review.forbidden'
180
+ : 'organizationDirectory.review.failed',
181
+ )}
182
+ </Text>
183
+ ) : null}
184
+ {(['approved', 'rejected'] as const).map((decision) => (
185
+ <Button
186
+ key={decision}
187
+ disabled={
188
+ !canUsePrivateApi ||
189
+ selfReview ||
190
+ detail.data.status !== 'pending' ||
191
+ !evidence.trim() ||
192
+ review.isPending ||
193
+ conflict ||
194
+ forbidden
195
+ }
196
+ onPress={() => review.mutate(decision)}
197
+ >
198
+ {t(`organizationDirectory.review.${decision}`)}
199
+ </Button>
200
+ ))}
201
+ {conflict ? (
202
+ <Button
203
+ onPress={async () => {
204
+ await queryClient.invalidateQueries({ queryKey: ['oxy', 'organizations'] });
205
+ onClose();
206
+ }}
207
+ >
208
+ {t('organizationDirectory.retry')}
209
+ </Button>
210
+ ) : null}
211
+ </>
212
+ ) : null}
213
+ <Button appearance="outline" onPress={onClose} disabled={review.isPending}>
214
+ {t('organizationDirectory.back')}
215
+ </Button>
216
+ </View>
217
+ );
218
+ }