@repzo/widget-react-native 1.0.4 → 1.0.5

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 (58) hide show
  1. package/lib/commonjs/state/widget-store.js +19 -4
  2. package/lib/commonjs/state/widget-store.js.map +1 -1
  3. package/lib/commonjs/ui/WidgetProvider.js +80 -64
  4. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  5. package/lib/commonjs/ui/components/Gradient.js +18 -2
  6. package/lib/commonjs/ui/components/Gradient.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/use-top-inset.js +22 -0
  8. package/lib/commonjs/ui/hooks/use-top-inset.js.map +1 -0
  9. package/lib/commonjs/ui/screens/chat/ChatView.js +8 -1
  10. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  11. package/lib/commonjs/ui/screens/home/HomeView.js +42 -9
  12. package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
  13. package/lib/commonjs/ui/screens/home/HomeView.styles.js +5 -3
  14. package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
  15. package/lib/module/state/widget-store.js +19 -4
  16. package/lib/module/state/widget-store.js.map +1 -1
  17. package/lib/module/ui/WidgetProvider.js +83 -67
  18. package/lib/module/ui/WidgetProvider.js.map +1 -1
  19. package/lib/module/ui/components/Gradient.js +18 -2
  20. package/lib/module/ui/components/Gradient.js.map +1 -1
  21. package/lib/module/ui/hooks/use-top-inset.js +17 -0
  22. package/lib/module/ui/hooks/use-top-inset.js.map +1 -0
  23. package/lib/module/ui/screens/chat/ChatView.js +9 -2
  24. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  25. package/lib/module/ui/screens/home/HomeView.js +44 -12
  26. package/lib/module/ui/screens/home/HomeView.js.map +1 -1
  27. package/lib/module/ui/screens/home/HomeView.styles.js +4 -2
  28. package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
  29. package/lib/typescript/commonjs/state/widget-store.d.ts +7 -0
  30. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/ui/components/Gradient.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts +11 -0
  34. package/lib/typescript/commonjs/ui/hooks/use-top-inset.d.ts.map +1 -0
  35. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -0
  37. package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +3 -1
  39. package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  40. package/lib/typescript/module/state/widget-store.d.ts +7 -0
  41. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  43. package/lib/typescript/module/ui/components/Gradient.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/hooks/use-top-inset.d.ts +11 -0
  45. package/lib/typescript/module/ui/hooks/use-top-inset.d.ts.map +1 -0
  46. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  47. package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -0
  48. package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
  49. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +3 -1
  50. package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/src/state/widget-store.ts +19 -4
  53. package/src/ui/WidgetProvider.tsx +41 -4
  54. package/src/ui/components/Gradient.tsx +19 -2
  55. package/src/ui/hooks/use-top-inset.ts +16 -0
  56. package/src/ui/screens/chat/ChatView.tsx +9 -2
  57. package/src/ui/screens/home/HomeView.styles.ts +4 -2
  58. package/src/ui/screens/home/HomeView.tsx +36 -8
@@ -1 +1 @@
1
- {"version":3,"file":"WidgetProvider.d.ts","sourceRoot":"","sources":["../../../../src/ui/WidgetProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwCH,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;CAChD;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,iBAA+B,GAChC,EAAE,wBAAwB,+BA6Z1B"}
1
+ {"version":3,"file":"WidgetProvider.d.ts","sourceRoot":"","sources":["../../../../src/ui/WidgetProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkDH,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,WAAW,GAAG,YAAY,CAAC;CAChD;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,iBAA+B,GAChC,EAAE,wBAAwB,+BAwb1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"Gradient.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/Gradient.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4BhF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,GACN,EAAE;IACD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAmBA;AAoCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAqB,EACrB,MAAW,EACX,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,+BA6BA"}
1
+ {"version":3,"file":"Gradient.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/Gradient.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,EAAoB,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4BhF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,GACN,EAAE;IACD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAmBA;AAoDD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAqB,EACrB,MAAW,EACX,KAAK,EACL,KAAK,GACN,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,+BA8BA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * How far a screen's header must reach up to sit under the status bar.
3
+ *
4
+ * The messenger draws edge to edge. Screens with a brand header (home, chat)
5
+ * extend that header's background into the status-bar area instead of leaving
6
+ * a white strip above it; the provider gives them the inset through this
7
+ * context and pads every other screen itself, so this is 0 for them.
8
+ */
9
+ export declare const TopInsetContext: import("react").Context<number>;
10
+ export declare function useTopInset(): number;
11
+ //# sourceMappingURL=use-top-inset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-top-inset.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/use-top-inset.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,eAAe,iCAAmB,CAAC;AAEhD,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BAwhBf"}
1
+ {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BA8hBf"}
@@ -12,6 +12,13 @@
12
12
  * the visitor already has; nothing here assumes a field the backend does not
13
13
  * send today.
14
14
  */
15
+ import type { WidgetConfig } from '../../../types';
15
16
  import type { HomeViewProps } from './HomeView.types';
17
+ /**
18
+ * The admin's explicit black/white toggle always wins. Older configs can omit
19
+ * it, and defaulting to dark text over a dark brand header is unreadable, so
20
+ * derive something legible from the background instead.
21
+ */
22
+ export declare function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string;
16
23
  export declare function HomeView({ config, color, visitorName, conversationCount, recentConversations, unreadByConversation, teamAgents, isOnline, offlineMessage, startConversationLabel, replyTimeLabel, allowNewConversations, showPreviousConversations, labels, onClose, onStartConversation, onViewConversations, onOpenConversation, }: HomeViewProps): import("react").JSX.Element;
17
24
  //# sourceMappingURL=HomeView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HomeView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAqCH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAiBtD,wBAAgB,QAAQ,CAAC,EACvB,MAAM,EACN,KAAK,EACL,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,MAAM,EACN,OAAO,EACP,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,EAAE,aAAa,+BAkNf"}
1
+ {"version":3,"file":"HomeView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA2BnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAQjG;AAED,wBAAgB,QAAQ,CAAC,EACvB,MAAM,EACN,KAAK,EACL,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,EACpB,UAAU,EACV,QAAQ,EACR,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,MAAM,EACN,OAAO,EACP,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,EAAE,aAAa,+BAmNf"}
@@ -16,6 +16,9 @@ export declare const START_CARD_LIFT = 28;
16
16
  export declare const START_CARD_MIN_HEIGHT = 76;
17
17
  export declare const HEADER_FADE_HEIGHT = 90;
18
18
  export declare const CLOSE_BUTTON_SIZE = 28;
19
+ /** The logo keeps its own shape at this height, up to the web widget's max width. */
20
+ export declare const LOGO_HEIGHT = 40;
21
+ export declare const LOGO_MAX_WIDTH = 152;
19
22
  export declare const CLOSE_ICON_SIZE = 14;
20
23
  export declare const styles: {
21
24
  contentContainer: {
@@ -47,7 +50,6 @@ export declare const styles: {
47
50
  minHeight: number;
48
51
  };
49
52
  logo: {
50
- width: number;
51
53
  height: number;
52
54
  };
53
55
  closeButton: {
@@ -1 +1 @@
1
- {"version":3,"file":"HomeView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0NjB,CAAC"}
1
+ {"version":3,"file":"HomeView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/home/HomeView.styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,qFAAqF;AACrF,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyNjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repzo/widget-react-native",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Repzo chat widget for React Native \u2014 pure JavaScript, no native modules",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
@@ -35,7 +35,7 @@ import type {
35
35
  } from '../types';
36
36
  import { setOpenUrlHandler } from '../ui/utils/safe-url';
37
37
 
38
- const SDK_VERSION = '@repzo/widget-react-native@1.0.4';
38
+ const SDK_VERSION = '@repzo/widget-react-native@1.0.5';
39
39
 
40
40
  /** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
41
41
  function sessionPlatform(): 'ios' | 'android' | 'web' {
@@ -489,7 +489,7 @@ export class WidgetStore {
489
489
  loading: false,
490
490
  error: null,
491
491
  errorCode: null,
492
- config: result.config,
492
+ config: this.hydrateConfig(result.config),
493
493
  identityId: result.identityId,
494
494
  conversations,
495
495
  // Also seeds the launcher badge, so a reply that landed while the app
@@ -512,7 +512,7 @@ export class WidgetStore {
512
512
  },
513
513
  }));
514
514
 
515
- this.persistConfig(result.config);
515
+ this.persistConfig(this.hydrateConfig(result.config));
516
516
  this.connectRealtime(result.token);
517
517
  this.watchAppState();
518
518
  this.loadOutboundMessage();
@@ -558,13 +558,28 @@ export class WidgetStore {
558
558
  cached.platform === sessionPlatform() &&
559
559
  !this.state.config
560
560
  ) {
561
- this.setState({ config: cached.config });
561
+ this.setState({ config: this.hydrateConfig(cached.config) });
562
562
  }
563
563
  } catch {
564
564
  // Corrupt cache — the network config overwrites it shortly.
565
565
  }
566
566
  }
567
567
 
568
+ /**
569
+ * The server sends the home logo and background image as paths relative to
570
+ * Workstation (`/api/media/:id/download`), which a React Native Image can't
571
+ * load. Resolved against the boot `baseUrl` they become full links (public
572
+ * media redirects to the CDN). Absolute URLs pass through unchanged.
573
+ */
574
+ private hydrateConfig(config: WidgetConfig): WidgetConfig {
575
+ if (!config) return config;
576
+ return {
577
+ ...config,
578
+ homeLogoUrl: this.client.resolveUrl(config.homeLogoUrl),
579
+ homeBackgroundImageUrl: this.client.resolveUrl(config.homeBackgroundImageUrl),
580
+ };
581
+ }
582
+
568
583
  private persistConfig(config: WidgetConfig): void {
569
584
  if (!this.bootConfig) return;
570
585
  const entry: CachedConfig = {
@@ -12,6 +12,7 @@ import {
12
12
  Modal,
13
13
  Platform,
14
14
  Pressable,
15
+ StatusBar,
15
16
  StyleSheet,
16
17
  Text,
17
18
  View,
@@ -35,13 +36,22 @@ import { InAppNotification } from './components/InAppNotification';
35
36
  import { OutboundMessageCard } from './components/OutboundMessageCard';
36
37
  import { WidgetPanel } from './components/WidgetPanel';
37
38
  import { useKeyboardOverlap } from './hooks/use-keyboard-overlap';
39
+ import { TopInsetContext } from './hooks/use-top-inset';
38
40
  import { useWidgetInsets } from './hooks/use-widget-insets';
39
41
  import { ChatView } from './screens/chat/ChatView';
40
42
  import { ConversationsList } from './screens/conversations/ConversationsList';
41
- import { HomeView } from './screens/home/HomeView';
43
+ import { HomeView, resolveHeaderTextColor } from './screens/home/HomeView';
42
44
  import { PreChatForm } from './screens/pre-chat/PreChatForm';
43
45
  import { globalStyles } from './styles/global';
44
- import { buildTheme, palette, radius, spacing, fontSize, withAlpha } from './styles/theme';
46
+ import {
47
+ buildTheme,
48
+ contrastingTextColor,
49
+ palette,
50
+ radius,
51
+ spacing,
52
+ fontSize,
53
+ withAlpha,
54
+ } from './styles/theme';
45
55
  import { pickRecentConversations, pickTeamAgents } from './utils/conversations';
46
56
 
47
57
  export interface RepzoWidgetProviderProps {
@@ -192,6 +202,21 @@ export function RepzoWidgetProvider({
192
202
  : null;
193
203
  const isHomeSurface = state.view === 'home';
194
204
 
205
+ // Home and chat paint their brand header under the status bar; every other
206
+ // screen (and an outbound card stacked above home) keeps a plain white top.
207
+ const outboundVisible =
208
+ !!state.outboundMessage && !blockingError && state.view !== 'chat' && state.view !== 'prechat';
209
+ const headerOwnsTopInset =
210
+ !blockingError &&
211
+ !(state.loading && !state.config) &&
212
+ !outboundVisible &&
213
+ (state.view === 'chat' || isHomeSurface);
214
+ const topBarColor = !headerOwnsTopInset
215
+ ? palette.ink
216
+ : state.view === 'chat'
217
+ ? contrastingTextColor(theme.primaryColor)
218
+ : resolveHeaderTextColor(localizedConfig, theme.primaryColor);
219
+
195
220
  const renderContent = () => {
196
221
  if (state.loading && !state.config) {
197
222
  return (
@@ -405,8 +430,19 @@ export function RepzoWidgetProvider({
405
430
  animationType="slide"
406
431
  presentationStyle={Platform.OS === 'ios' ? presentationStyle : undefined}
407
432
  onRequestClose={handleClose}
408
- statusBarTranslucent={false}>
409
- <View style={[globalStyles.screen, { paddingTop: topInset, paddingBottom: insets.bottom }]}>
433
+ // Edge to edge on every Android version, so the insets below are the
434
+ // only top spacing (a non-translucent modal used to add a second one).
435
+ statusBarTranslucent>
436
+ <StatusBar
437
+ barStyle={topBarColor === palette.white ? 'light-content' : 'dark-content'}
438
+ animated
439
+ />
440
+ <View
441
+ style={[
442
+ globalStyles.screen,
443
+ { paddingTop: headerOwnsTopInset ? 0 : topInset, paddingBottom: insets.bottom },
444
+ ]}>
445
+ <TopInsetContext.Provider value={headerOwnsTopInset ? topInset : 0}>
410
446
  <View
411
447
  onLayout={keyboard.onLayout}
412
448
  style={[globalStyles.fill, { paddingBottom: keyboard.overlap }]}>
@@ -463,6 +499,7 @@ export function RepzoWidgetProvider({
463
499
  </WidgetPanel>
464
500
  </WidgetErrorBoundary>
465
501
  </View>
502
+ </TopInsetContext.Provider>
466
503
  </View>
467
504
  </Modal>
468
505
  </>
@@ -104,6 +104,22 @@ function GradientFill({ colors, bands }: { colors: string[]; bands?: number }) {
104
104
  );
105
105
  }
106
106
 
107
+ /**
108
+ * Alpha at evenly spaced points down the fade. An eased curve instead of a
109
+ * straight line: a linear ramp shows a visible edge where it starts and goes
110
+ * opaque too late, so the brand colour reads as a band rather than melting
111
+ * into the page. Shared with the web widget's fade so both look the same.
112
+ */
113
+ const FADE_STOPS = [0, 0.04, 0.12, 0.26, 0.46, 0.68, 0.86, 0.96, 1];
114
+
115
+ function fadeAlphaAt(progress: number): number {
116
+ const scaled = Math.min(Math.max(progress, 0), 1) * (FADE_STOPS.length - 1);
117
+ const index = Math.min(Math.floor(scaled), FADE_STOPS.length - 2);
118
+ const from = FADE_STOPS[index] as number;
119
+ const to = FADE_STOPS[index + 1] as number;
120
+ return from + (to - from) * (scaled - index);
121
+ }
122
+
107
123
  /**
108
124
  * Washes the bottom of a header out into the page colour: a straight ramp from
109
125
  * fully transparent at the top to opaque at the bottom edge, the same
@@ -129,7 +145,8 @@ export function FadeOverlay({
129
145
  <View style={[styles.fadeOverlay, { height }, style]} pointerEvents="none">
130
146
  {NativeGradient ? (
131
147
  <NativeGradient
132
- colors={[withAlpha(color, 0), color]}
148
+ colors={FADE_STOPS.map((alpha) => withAlpha(color, alpha))}
149
+ locations={FADE_STOPS.map((_, index) => index / (FADE_STOPS.length - 1))}
133
150
  start={VERTICAL.start}
134
151
  end={VERTICAL.end}
135
152
  style={StyleSheet.absoluteFill}
@@ -144,7 +161,7 @@ export function FadeOverlay({
144
161
  // body colour, with no hairline where the two meet.
145
162
  <View
146
163
  key={index}
147
- style={[styles.band, { backgroundColor: withAlpha(color, (index + 1) / all.length) }]}
164
+ style={[styles.band, { backgroundColor: withAlpha(color, fadeAlphaAt((index + 1) / all.length)) }]}
148
165
  />
149
166
  ))
150
167
  )}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How far a screen's header must reach up to sit under the status bar.
3
+ *
4
+ * The messenger draws edge to edge. Screens with a brand header (home, chat)
5
+ * extend that header's background into the status-bar area instead of leaving
6
+ * a white strip above it; the provider gives them the inset through this
7
+ * context and pads every other screen itself, so this is 0 for them.
8
+ */
9
+
10
+ import { createContext, useContext } from 'react';
11
+
12
+ export const TopInsetContext = createContext(0);
13
+
14
+ export function useTopInset(): number {
15
+ return useContext(TopInsetContext);
16
+ }
@@ -51,8 +51,9 @@ import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } f
51
51
  import { TeamAvatars } from '../../components/TeamAvatars';
52
52
  import { Markdown } from '../../components/Markdown';
53
53
  import { useReduceMotion } from '../../hooks/use-reduce-motion';
54
+ import { useTopInset } from '../../hooks/use-top-inset';
54
55
  import { globalStyles } from '../../styles/global';
55
- import { contrastingTextColor, mixHex, palette, withAlpha } from '../../styles/theme';
56
+ import { contrastingTextColor, mixHex, palette, spacing, withAlpha } from '../../styles/theme';
56
57
  import { formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
57
58
  import { stripHtml } from '../../utils/html';
58
59
  import { isHttpUrl, openExternalUrl } from '../../utils/safe-url';
@@ -327,6 +328,7 @@ export function ChatView({
327
328
  .find((message) => message.direction === 'inbound' && (message.messageType ?? 'message') === 'message')?.id;
328
329
  // Two stops off the brand color: flat fill looked like an unstyled block.
329
330
  // A non-hex brand color falls through mixHex unchanged and renders solid.
331
+ const topInset = useTopInset();
330
332
  const headerGradient = [color, mixHex(color, '#000000', 0.16)];
331
333
  const headerControl = { backgroundColor: withAlpha(headerTextColor, 0.16) };
332
334
 
@@ -336,7 +338,12 @@ export function ChatView({
336
338
  was empty whenever no agent was assigned yet. */}
337
339
  <GradientBackground
338
340
  colors={headerGradient}
339
- style={[styles.header, { borderBottomColor: mixHex(color, '#000000', 0.26) }]}>
341
+ style={[
342
+ styles.header,
343
+ { borderBottomColor: mixHex(color, '#000000', 0.26) },
344
+ // Reaches under the status bar so the brand colour runs to the top.
345
+ topInset > 0 ? { paddingTop: spacing.md + topInset, minHeight: 72 + topInset } : null,
346
+ ]}>
340
347
  <Pressable
341
348
  onPress={onBack}
342
349
  accessibilityRole="button"
@@ -23,6 +23,9 @@ export const START_CARD_LIFT = 28;
23
23
  export const START_CARD_MIN_HEIGHT = 76;
24
24
  export const HEADER_FADE_HEIGHT = 90;
25
25
  export const CLOSE_BUTTON_SIZE = 28;
26
+ /** The logo keeps its own shape at this height, up to the web widget's max width. */
27
+ export const LOGO_HEIGHT = 40;
28
+ export const LOGO_MAX_WIDTH = 152;
26
29
  export const CLOSE_ICON_SIZE = 14;
27
30
 
28
31
  export const styles = StyleSheet.create({
@@ -58,8 +61,7 @@ export const styles = StyleSheet.create({
58
61
  minHeight: 40,
59
62
  },
60
63
  logo: {
61
- width: 132,
62
- height: 40,
64
+ height: LOGO_HEIGHT,
63
65
  },
64
66
  closeButton: {
65
67
  marginStart: 'auto',
@@ -13,7 +13,7 @@
13
13
  * send today.
14
14
  */
15
15
 
16
- import { Fragment, useEffect, useRef, type ReactNode } from 'react';
16
+ import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react';
17
17
  import {
18
18
  Animated,
19
19
  Easing,
@@ -37,6 +37,7 @@ import {
37
37
  CloseIcon,
38
38
  } from '../../components/icons';
39
39
  import { useReduceMotion } from '../../hooks/use-reduce-motion';
40
+ import { useTopInset } from '../../hooks/use-top-inset';
40
41
  import { globalStyles } from '../../styles/global';
41
42
  import { contrastingTextColor, palette, spacing, withAlpha } from '../../styles/theme';
42
43
  import { flipForRTL } from '../../utils/rtl';
@@ -45,6 +46,9 @@ import {
45
46
  CLOSE_ICON_SIZE,
46
47
  HEADER_BOTTOM_PADDING,
47
48
  HEADER_FADE_HEIGHT,
49
+ HEADER_MIN_HEIGHT,
50
+ LOGO_HEIGHT,
51
+ LOGO_MAX_WIDTH,
48
52
  START_CARD_LIFT,
49
53
  styles,
50
54
  } from './HomeView.styles';
@@ -55,7 +59,7 @@ import type { HomeViewProps } from './HomeView.types';
55
59
  * it, and defaulting to dark text over a dark brand header is unreadable, so
56
60
  * derive something legible from the background instead.
57
61
  */
58
- function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
62
+ export function resolveHeaderTextColor(config: WidgetConfig | null, fallbackColor: string): string {
59
63
  if (config?.homeHeaderTextColor === 'white') return palette.white;
60
64
  if (config?.homeHeaderTextColor === 'black') return palette.ink;
61
65
  if (config?.homeBackgroundType === 'image') return palette.white;
@@ -116,8 +120,14 @@ export function HomeView({
116
120
 
117
121
  // The start card is pulled up over the seam by a fixed amount, so the header
118
122
  // pads its bottom by that same amount and the greeting never slides under it.
123
+ // The header reaches up under the status bar (no white strip above it), so
124
+ // it grows by that inset and keeps its content where it was.
125
+ const topInset = useTopInset();
119
126
  const headerStyle = [
120
127
  styles.header,
128
+ topInset > 0
129
+ ? { paddingTop: spacing.xl + topInset, minHeight: HEADER_MIN_HEIGHT + topInset }
130
+ : null,
121
131
  allowNewConversations ? { paddingBottom: HEADER_BOTTOM_PADDING + START_CARD_LIFT } : null,
122
132
  ];
123
133
 
@@ -125,12 +135,7 @@ export function HomeView({
125
135
  <>
126
136
  <View style={styles.headerTop}>
127
137
  {config?.homeLogoUrl ? (
128
- <Image
129
- source={{ uri: config.homeLogoUrl }}
130
- style={styles.logo}
131
- resizeMode="contain"
132
- accessibilityIgnoresInvertColors
133
- />
138
+ <HomeLogo key={config.homeLogoUrl} uri={config.homeLogoUrl} />
134
139
  ) : (
135
140
  <View />
136
141
  )}
@@ -296,6 +301,29 @@ export function HomeView({
296
301
  );
297
302
  }
298
303
 
304
+ /**
305
+ * The workspace logo at its own shape, flush with the greeting's left edge.
306
+ * A fixed-width box with `contain` centred a square logo inside it, so it
307
+ * floated in from the edge; the web widget sizes it to the image instead.
308
+ * Square until the image reports its size, then as wide as its ratio needs,
309
+ * capped like the web widget.
310
+ */
311
+ function HomeLogo({ uri }: { uri: string }) {
312
+ const [aspectRatio, setAspectRatio] = useState(1);
313
+ return (
314
+ <Image
315
+ source={{ uri }}
316
+ style={[styles.logo, { width: Math.min(LOGO_MAX_WIDTH, LOGO_HEIGHT * aspectRatio) }]}
317
+ resizeMode="contain"
318
+ accessibilityIgnoresInvertColors
319
+ onLoad={(event) => {
320
+ const { width, height } = event.nativeEvent.source ?? {};
321
+ if (width && height) setAspectRatio(width / height);
322
+ }}
323
+ />
324
+ );
325
+ }
326
+
299
327
  /**
300
328
  * Staggered entrance for the body sections. Opacity and translate only, so the
301
329
  * whole thing runs on the native driver and never touches layout.