From 202bb27ae743e4e11be8afc344624f4ff45d493a Mon Sep 17 00:00:00 2001 From: Yug2801 Date: Mon, 27 Apr 2026 20:34:39 +0530 Subject: [PATCH 1/3] [skip ci] Release v2.3.1 This PR contains the automated release updates for version 2.3.1. Changes: - Updated sample app - Updated documentation - Updated README, CHANGELOG, and MIGRATION guide --- CHANGELOG.md | 21 ++ README.md | 85 ++++- devrev-sdk-react-native-2.3.1.tgz | Bin 0 -> 50039 bytes sample/expo/PushNotificationsService.tsx | 2 + sample/expo/app.json | 18 +- .../components/TouchableOpacityButton.tsx | 13 +- sample/expo/navigator/Navigator.tsx | 14 + sample/expo/package.json | 6 +- sample/expo/screens/CameraScreen.tsx | 190 +++++++++++ sample/expo/screens/FlatListScreen.tsx | 2 +- sample/expo/screens/ImageUpload.tsx | 244 ++++++++++++++ .../expo/screens/PushNotificationsScreen.tsx | 46 +-- .../expo/screens/SessionAnalyticsScreen.tsx | 21 ++ sample/expo/tsconfig.json | 4 +- sample/package.json | 2 +- .../android/app/src/main/AndroidManifest.xml | 5 + .../xcshareddata/swiftpm/Package.resolved | 4 +- sample/react-native/ios/Podfile.lock | 56 ++++ sample/react-native/ios/Sources/Info.plist | 6 + .../ios/Sources/PrivacyInfo.xcprivacy | 1 + sample/react-native/package.json | 2 + sample/react-native/src/App.tsx | 14 + .../src/components/TouchableOpacityButton.tsx | 9 +- .../react-native/src/screens/CameraScreen.tsx | 162 ++++++++++ .../src/screens/DelayedScreen.tsx | 3 +- .../src/screens/FlatListScreen.tsx | 2 +- .../react-native/src/screens/HomeScreen.tsx | 2 +- .../src/screens/ImageUploadScreen.tsx | 300 ++++++++++++++++++ .../src/screens/SessionAnalyticsScreen.tsx | 22 ++ sample/react-native/tsconfig.json | 9 +- 30 files changed, 1208 insertions(+), 57 deletions(-) create mode 100644 devrev-sdk-react-native-2.3.1.tgz create mode 100644 sample/expo/screens/CameraScreen.tsx create mode 100644 sample/expo/screens/ImageUpload.tsx create mode 100644 sample/react-native/src/screens/CameraScreen.tsx create mode 100644 sample/react-native/src/screens/ImageUploadScreen.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4c6a0..0a281a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.3.0] - 2026-04-27 + +### Fixed +- Improved session recording stability and reduced resource usage on both platforms. +- Fixed multiple crashes and ANRs during session recording and app termination. +- Fixed incorrect session engagement time calculations on Android. + +## [2.2.6] - 2026-02-16 + +### Fixed +- Fixed an issue with session recordings on QR scan screens on Android. + +## [2.2.5] - 2026-02-02 + +### Changed +- Modernized legacy components and aligned for better performance. + +### Removed +- Removed deprecated and non-public iOS APIs. + ## [2.2.4] - 2026-01-29 ### Changed diff --git a/README.md b/README.md index 67e2222..d4fa550 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ DevRev SDK, used for integrating DevRev services into your React Native and Expo - [Installation](#installation) - [Expo](#expo) - [Set up the DevRev SDK](#set-up-the-devrev-sdk) + - [Update the feature configuration](#update-the-feature-configuration) + - [Feature configuration reference](#feature-configuration-reference) + - [Support widget theme options](#support-widget-theme-options) - [Features](#features) - [Identification](#identification) - [Identify an unverified user](#identify-an-unverified-user) @@ -60,7 +63,6 @@ DevRev SDK, used for integrating DevRev services into your React Native and Expo - For Expo apps, Expo 50.0.0 or later. - Android: minimum API level 24. - iOS: minimum deployment target 15.1. -- (Recommended) An SSH key configured locally and registered with [GitHub](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh). ### Installation @@ -100,12 +102,89 @@ npm install @devrev/sdk-react-native > [!WARNING] > The DevRev SDK must be configured before you can use any of its features. -The SDK becomes ready for use once the following configuration method is executed. +The SDK becomes ready for use once the configuration API is executed. ```typescript -DevRev.configure(appID: string) +DevRev.configure(appID) ``` +To provide a feature configuration during setup, call the overload that accepts it: + +```typescript +DevRev.configure(appID, featureConfiguration) +``` + +For default behavior, call the simpler form: + +```typescript +DevRev.configure('abcdefg12345') +``` + +To customize behavior such as frame capture, auto-start recording, or theme preferences, pass a full `FeatureConfiguration` object: + +```typescript +DevRev.configure('abcdefg12345', { + enableFrameCapture: false, + autoStartRecording: false, + prefersDialogMode: false, + alwaysUseRemoteConfig: true, + supportWidgetTheme: { + prefersSystemTheme: true, + }, +}); +``` + +#### Update the feature configuration + +You can adjust the feature configuration without reconfiguring the SDK. Pass a **full** `FeatureConfiguration` object (all properties are required): + +```typescript +DevRev.updateFeatureConfiguration({ + enableFrameCapture: true, + autoStartRecording: true, + prefersDialogMode: false, + alwaysUseRemoteConfig: true, + supportWidgetTheme: { + prefersSystemTheme: true, + }, +}); +``` + +#### Feature configuration reference + +`FeatureConfiguration` controls how the SDK behaves both during initial setup and when calling `DevRev.updateFeatureConfiguration(...)`. All properties are required when providing a feature configuration. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `enableFrameCapture` | `boolean` | `true` | Enables the screen capture pipeline used by session replay. | +| `autoStartRecording` | `boolean` | `true` | Automatically starts recording after the SDK finishes remote configuration. | +| `prefersDialogMode` | `boolean` | `false` | Prefer dialog mode for the support UI (Android only). | +| `alwaysUseRemoteConfig` | `boolean` | `true` | Always use remote config. | +| `supportWidgetTheme` | `SupportWidgetTheme` | — | Controls the appearance of the in-app support widget, including dynamic theme behavior. | + +##### Support widget theme options + +`SupportWidgetTheme` lets you fine-tune the support UI. Use the `supportWidgetTheme` property inside your feature configuration. + +```typescript +const customTheme = { + prefersSystemTheme: false, + primaryTextColor: '#1F2933', + accentColor: '#F97316', + spacing: { + bottom: '20px', + side: '16px', + }, +}; +``` + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `prefersSystemTheme` | `boolean` | `true` | Follows the device appearance when `true`; otherwise uses your custom colors. | +| `primaryTextColor` | `string?` | — | Hex color string (e.g. `'#000000'`, `'#1F2933'`) for primary text in the support widget. | +| `accentColor` | `string?` | — | Hex color string (e.g. `'#F97316'`, `'#FF0000'`) applied to buttons and highlights. | +| `spacing` | `{ [key: string]: string }?` | — | CSS-like spacing overrides (`bottom` and `side` keys are recognized). | + ## Features ### Identification diff --git a/devrev-sdk-react-native-2.3.1.tgz b/devrev-sdk-react-native-2.3.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a9c8a5fa67b2fbba8b33d913b56f0d10c2c2e9ba GIT binary patch literal 50039 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Ü~ zCYM~+1%z&ZXAMS5s&Uz(NgIB$Mz5th!Zf*2ACoKri3S%MX(=Xs^2c`3#p|AX_k;?%J%APUro*V zB8>z-F*5$pfHkDfPkcYx2uKIPN_f+2l@T4D^5S-%&t8;i`X}m?e|$s9mhH^!0 zSoQ7Ow;9pdBu?$&c|a=r=OEDeiOVqYaXPWmleJ)(!b1>hr(%_gsxq02m;j6FQ=k^9 z+sM`*rAGTRljuj7KqC9M3?pwH5#CMu+X`$SS15lELEp>Gx4;2k+(d4CO5G+lred(7 zsD9RXt{GS6f~siDqC1)I_4$1n!vczySUTm4r_eFcrH~t9g`zw(DTWBssZAztB$mW7 zMB$2bPFWdC^wIX1Erp`@n1G;jo}=`FP8H{pMWXqLZBe3rOozW$q(n}HSt*Bl)&plY zu1)Y>_uD*ekajkkomusa{mse>tGBWu_c$!MHJ!{C`!U#c_Apz-ie5EveOJaS0ua@= z$9{&Vb5r-&2i*EHt{+HhnRW`IG!A&b{rpF$^JbqJm|WHJ^EmmDI^?y2Lp-9pLOEt& z6N-8Kh2?`W4h&WxCiSHN6%n?85zw4~o?X=AS%9sPDz7mLE`tpIWw>jsZLhDcZat|h zhyV(Pf`&5*-VZZ?z=J7SlrNnW>#75%T(BgK945a|m~_gKT-yCP$C}Fmk_86R20yft zHdy}hx{lTKivhEG0v_sNaPD`<-Ow+cfe&->F>`}d1pym8oPRfcev`cVz}CD;d8mz7I?%7tsms8a4EEgcm|kWRp!_=>q9gK&BAHOB z-aUYl^Us>F6kb};!xek!qLuf0+zu-x4qpq7N=w=>c7IS#^h?{&Hx14iM205eG?~%t z^-}#6=Mg~(LcJER#etxdlF?BLW0nAo$waycTPuXQYPKMu)>%Sz(Lm%8Eo3AN1Rw;nR5as?mlG9nY-9`q>8s!b3)o z_KRzeEo~GTB=<)kXwW8KL{g0^;oh)IS^P__TyC4H*B&LQy_Wsf|6yY)V50e#g;wHhdxGgSl-n}9gv=cY1&vyDSX7z zSxfbhwQ0kubzCNDixCf1?8889SsBA7M6na4I6Cv$Vphb|m>~(1euC&C3;wL=s)=l# z88(AHd~ZZ|z8Lr@#U+~}owLcjz|nL|eA;42iWn^~HJw~pbdI=&%cg5)RKjD~pogH~ zWZE`?-ALGDH{T3=k7=4v14U4RI~hvj#&#DX6)GxjEQu`TPh=Hro0iD;dnwod&%6+m zrtx%oAP3)yxWuBG{Z{!x!_dCfX!3F4PqqlE-y~*9OCAle_w}t+mE$xm-+`zl3}kC@ z#tl?kH0F}Q6tw4(aytKXq8{mgcyl7VkBn;KDvXtFxF)^+?%+D9!D4eZW+T0k#o-T;@&qlmf@|vA|Ett$(hU}y}DL5#qqwd5U1{iogP1N&+D3xjJ zX^Tyv-Wi+fRXqU)0b@}?nPr=5Tob87-WP%-dgDEzPOSQ%oh&xUrU<*R zI2J7<&Ea=SKzo(IGjTp!mFe@q^N}Dn2l*JZlIf7fK5;yc)s^XHO}EEnOasUI=#}mI zx%=VGHhDztoBf)wZ>allc~P1RpXK-|PQrv?7UrUOJi$HzrR8SWA+x}Hd9$yvGmb`d z15U=B9!|;%ld4CKNyEb>#>%qX(;ktNZ-|myY~o0gQoHP6S&uf_)N-nB*CH%~_^7eu z_nPKnT5fPxLa~y#In?2Gy8MbW-n1)qY-J+BJmYMeSgcM2#GzvLD4|&~j_ZvQdAAOX zs?N`#3GaBB^2|d*81eD-MMJACCg#Lc5DOvLxw8?`E1@L@%r{SO zkgyhHa_?l4sxvP05X;=tOB1O&E4s|=#ioms*M;Z~U7{TEC=nsFDN=)l+sTk3KDF!| z3hG64mW1E4R31dacEo%LwkQ!1=Xa9t*-NK&?*2VPP8RVdf3A3wBJbt*eua~xRgE`l zjx`!(_FAURII;2i)L5{oY`oyS1?T>?Q@aZ(Hf`QSme%_tB~wsIlNG-(40Q< zVq>fV9yjw-{W1q0K)^L6I>2}lbT61)!B34=`MffPRHHmL5JqW=}O&eF1U~BMqRY^;6ExJeWnD6T_RAxZNt{1eP#Y<_!o-Eb->(Wr~`J zKcqr6$IrU1@Ql8GIeMYen`1u9{IN*h=B_%u!hJe)swKtd3$~ejvWhBiWLaiBw;zf> zJa(Qvdj=H87Q#m&i8Y^x(V4o$>zQ#V8lyB`(nWjF*`q*of5d?7J*x2t&1YZ9gvcDE zD)S4Dl91huMVYPeud*N&M#D>p#P7>`vpSHV9}eOxUzGpwl;>N<;l!{2UwcVZePnK^ z19pEepAvQ_6{tP`d_;}#E~8Nx&SSn3c`$OJ9ZbfZLFtHWqj&|N*h8yYGX?3AF~L%q z`Cu_}DswLUaF}w_z?*##k5~M1L+*AL-Z(!>`RQ+>v(Uft*nOicq!Mkre&~(qLa!_c z#hu2Y#~_vdV7m`wKNBmKRLCBs3B?h)3dFW6Z+pa1V&lF+5&aJ9xqm z^;xq`H64B_2X`t*Jpvam4>gRl0gJpX@<5&pPjoWO>le0p)!aY#Pc*`3bk9UHL$owI zKVY6;65CvlyxsL4C0HxC{&k81)wbUk7ALjoAQ5V9q{d_)Gtl#(Iu=9jvw#^g0tub7 zr%Ro1>D!oTRX-3AJ%tv z7Wtna^8U|1oMN)Anqy=tt2tt|sX3OsXU+#^N_u1@m3zAMnWe4!3uLOBkeqJTd~|=w z6m%@%oKwt1H#O_rBGKI6GtnHUm_@$1KT*E9{G2V)&Mk6#ed4rpd6HaYpIcDqLua2W zqrpWIxG_$N$BmuKfcL1wx<$e&Mwx`GX1 zk*Dr&nWrvK2a9xdQ*w0`So%+uwXO*LXOX-v57wv8VV8&K)2Fht;^=eDXlF;?BC*{f zvE5%kv0a|_Kkxi@1x0X?=I)-;+~qO6$aZ%x+3pI~okh~SzjD$$tKPlHefRcHG-Znv zc#9NxixhZMbBNjZPK!)<3mX12Wx}&rnidK1?mZ!10nLlNc%M2io@GiF>G2ln@jiZf zJgYBfktOdhmL+e(EH9GfEt2IeQg;14Qg#)0iHn?h3kF;;;3s3izhCOSLPWwMgWe*8 z-XeqEOd0eFZudnZz0Wd{p2e6mJ)hnph2A2C-XewGM@XS(4FZ^us%()wZ;?Fjfs*H0 zf*n34VJ_0<{Tb8dS+$87vga)l<}DKD-Alr}k}dn6E^nTm{|$HPar7>2uzW}|h0@@{(jiOf!#1ZpB7IzC#15jMa(e2g zER$EIMr2L$aMwZgZV5L?yr?U}5S}?#Xxv?Wx^(3wj-I?!%Y0SQGrCovl}5wak6=6M zqvY_JqcdP?6t6?RC?lU-H}3g;A65{k{5|Yuk-O7z)MZAiH0d_5t<=RsCIpKDu?V!0 zWwj)$T8CmJa8c0Ht39}ltkCc~|{Ezp{{ZsjWV`nGN|C?Len+yJb(EN{crF8tr z>?u}P!H<7s2K=gb71t8D=8yom+!T%4v{#!XmP{V>>{vP?C@0t6j z^Z(|0f&bzAg8v`lvtaoJ|0DmS##JANqka%IFqw6)fqEG?T71;~|IX@qasR)*y}IE4 zhhP6`Uwq2;1Z}w)u97Y7-~T3?nwxu1`NX9etO{$0Fr^AFC_KW-@W)l=gJtJyeB$iF z)}zAp{K_sp`tWoK{+rJdq1z!G-43CsSTjX1uQ3ZgIAuhip04QziEpregO%WHk)mOO z)jVgVbC{^g+UHPK79M)p#pL2xLaQL+w_ulY`eWzlc>nMKlYFnc>+ag>qe7{J=Ii5c z4^K{yIUoYh0uQ|Io4)l0qB6IfNql#$In1?DutmYnP z58#z5$KwJ;*{5aY_79I=$G!6)^k*q5qSef@oK)E}B0nqrs2{~)i7-an=wQp+?Dcs1 z5M-%P$hI2i(K^s5#Zhzi#FG!w-5p96U^AB`Fwd{fV~UKuWQC`L)>RQ~lxDQ$nK zXoabIivf@ui=^noC>&RpwRa!n_YZIR=g9Ea@ExEa}hmi4+GA znY*vz1uo-37kF@2vE#0WHzhefRP9UI+jyx+Vj<#}>SXaterR`xZj(CL2gJP6l zWjl4Jc@Ml$}4$w~A6gQeO~t!%@#q ztCbQV4vUac+d>LC%m{Fc6b@Pyp6FMWIlSEjZ)*W^|22YtK=C+5DJy8ld z1$g#_gW*?B7{GJCOKAVU7{DMhs;FEZUd;GvAp!JKXOZ5K`cy~iR&cD{Nhwd z5o@+og31q!7Jpkd7)>lxAo#UgCYC9m^Wm`rkK2Ce_tBRLOsIme(V-q>ffokvF#yBP zUb6USz+d8C6!XIsEkNmhbRIJbGeLo2a7l|uPthQ(A;rdyiVbrUW8pv1vy|P%8g|}# zR5EdoEW9;^zeFehwDiL5_M)>E&^z>CLF80Mad9f>A(qI0&{M*W}%OEJ~U%@|naU7!L_yhh~{xzz? zADpospYCN89{*bVHNsv?X5iQ8U+ZrzV9&VcnIZ{ zbH*^NIKe&HsLSq3xj=^NJ$2H{U^t<-nZ!%c)mhYZpDr!zB7ZmgkACsgefKfZ{sph6UH%4R?(_jksS`?OmV%6L}RL zTqhqC8`-J|MWHf)rC=}`{HPV!WZOM1!789v+_tH;Er54P$V_SwlwcW;k45w|^Ax2< z#|*WcF1$eMCwR~d^(=8Z(r|R+HD(MonDYu-HhilTxy9cz^A#_l-@eJ-lNDaPrXK`v z1D)k80IcU`DJyGC7gUjD*lT9tx4gh*4f|l~8@}ae#7@OmF^}3j@UPwBP<&l+K?hEU zzHH#1Oxu<}QQ;=jvV0;=IohB0$<$Hu_`2-5d*)?@ceFonRR%O!U}dFodYXZDn4X@R z56^=eI{cP@K8U-QxrgqG_@!CVk=lM^xTjgu*aG{*sd$Hfc}<+&?uFmI#1~oGb?Wsf z#xpExIwGp%r})kJwE`WlxPQD$YZ?A`^WoQuoD>Ya#j>Cs)w^Nf7>fR^m9U2I2!?2WREIMGDUe|UT_ z7sIX>eC1TN6T|G;GGp8jWKfh6bWK>iFZ}wtyUJc&!~=f_i#fiV8^$ltx-j4(e>ge| zK|g2vEQOYY2jS=)h-Ypz9K1{2IO#Vkny9_9uMpp$TnsWKX<=<=`M=olHSFNOYdh|$ zyDsqmjeiw{0}t5Pi_%8up92!(OIFa`;wZp!`k?CA%?S3z;|A~pEBqK*ouzbLm~rZA zIlEPLwtP*j+BoyhfM@9v3gY(vuDXvm$_g`4Q16P_!O}ZoVJ?)$+^DfxIM+0YjnuCP zG!t?P>TLTF#;nqo343%?$CL7281yl6e8@%!&AQvN5UpH|cU+8U@+g zRigv;mc}7)x9)CkN=YZF$3iJw`f;$=RI3bc^!%~ARd`VsZl44X*89Yv|9zr^#mg~? zRM$tgs<3YsG8)@5%As&$i+^1$6_x*PLZJ;&Nbh4^)iqk(&@1Hi0XExfw%2u%xqeay zVa$uJZ>qs6x?ngqC?LbgJkES;Kz;gh){DEMxoz_e8zx7^I?R|96hQcKI3fdpCM3$J zKvo<2m_9(vo!XS z=$j39Td2=`V*ily$r<0)1tSu)cJ24kzzN^*46lL@%KQgSp6wv{s$pB6b=$jAmkV{s zK02)|Dy2-N{!QY{;Kx4%eP=K)29VRFey&x60(q-_?T74`pO;4bbzy1scO72U`PUzO zx9~v?CG+|ReB*Pay*IWTu$bxH@b$351K#zLY=PAa^S*6`g-2N{J(K)ZrX-D2;Poi< z65$&AklW@kIaFA}VH!DPweoqgP@yh$*gOf28il?wB2lcNNhxI|uFwhhin0^yX0pof zqc|h4KgmKg2GbetOe>E(hX>O9AF!-37CFIZKLx!Er)%AOk$3?nery5Ncuc9@GFyfX z;E8vJozwZif&=Y0E0P&oDsiE+P(#~S$?u@UvtXN*73~o!(XU}IWj-}jbo(o*o13p! zT-XqUzHHg0@Zx&scvr~4@8Kz_90Vi3&NIMhjlpvjRDGTRL!sa_E8cWVUJU%C@3;N4 zQNNkuO@9SE)V*Y|y;<`3^ycyQ>Gozlh0T75cavi|C3_Xsahjh(RHfu(A*4wpWFejp zg*|$1(d7^t+jP-fJj#ZnjN1X^GD?mzgIk4(L9>JK_!$0MV?J#aF5y7#E#bQ`w&fz2 z<;Kg#5!zBf7n~Vei#oucc((l&SAWnR-W^LuLBR&%oM!7X#^s`(UI*uLzBB_6Xg)jo z;>{IP)f457CGX#m=NE2SKe!;Sc&!9h>38|5=>O$66k#GQh+agSa>(4 z#D5wlmX9C*y|J-cIRCZ2vA(eXKa~C7Q2uQDQAH8lwyCz?!yxn%?y}XH6c#Pc%({7x zE1nvgn+AU7-bQIwana0O`Aw=U>WyxOZGd9nRCFsYJo#CVfgEn@%rE?&79O;X^UZH`$C8dFeeb z^}hqD%tY;sbzf0Fl$i?m@Y04?-f;H9O}w0LuC~AT$Hy?KcsVJM;_Ov%CpV!q-&(l1 zzj0Z1SfWyHhMkJBQNYq3B`$O6BTarA0FbaEj(H9ZjsKC?dsC^ThT%hOg4G{c%(0=QOAh4H~x*=w5&8Yy@L?hWt9T2Sj_x?=!<@gT_SAC=N+-6LZ)XxlMMFPM0*hzT zqD1rJX*oVueh+s#aR^G|quXv2 z398VlY0@UwgmEur0jtF;(I?^8P>yXV&J~z|WkdV5Hf6k@(2}`}ur!q~Frls7Cc@HK zp~N)ECf_7qfU~stRH$d~XiE7>Bbt_k<~y!exZ0U->U9W_}c2-P6`pgI3bSJUY4 zF){o)_lKF9Qlw6r9Kl?Y@spVp?OOhI1eFPfXCP30~SELTCT*A3>ow_PwZ5tS2fty0rnrXk;o@%2fX z?bfWxyM&UPfbJXE(tc)8Ny@J0xn~fkT?SOxB3p06og{~BmtGY4#8zq&@6B{^?yj@P za*A6!oaN@-{O~7UKsy-=Wf#m&2D0>8+Hx@>62>#T8(NsBPf%&%^2yLm%6Og!kaloD zIP7MXS)tP}W1;l(2L+K=zKg&cQ$%gl3Pw@33lyAb^(1uWH9?b1YlTa9l+eq(zGh^R zx9?1_`;=ThWZc~&V9i?|TQOZ4l%;5Xgr)R9qUUBb{tASF>IDijtD;urwk~g*|2Oil z@6Y)=-T%8`@&B%`FYK#=Jl*t=9@`-Qvm-D^6j}$#w8CJUe&w?nqQj2K_#qJ$H8aNLbZe6x}|v zPR9Lsf$r!Dni2bVb`0IYIW#ATv-pbU^B2uUq0`+*Gr5pv>~o4oX{uMrLeQFTX;w@KWnZmby_ zFY#(k^=!@9e3_$by0dE@_@}v~=JbEfHMaTsp6(Kxb==cEWOI4RCh&ymE1Q)mZsj_g z+kG~xw43Zro7tmg#ZswXZB|s3IUY(~IWszQ?%>Y3g+nLRYu~@4Z8m?-yzVw5kL(-@ zN%VJ_vpf3WZZm7Mc;ZUuO)+M{CpXt}ej8WWTyDAbC`Pk%WB2yi#$ELJ&84|caRJWe2DHFscLvTrA9wT(-oe#3vpk*A^EZbhZ$3q8 zR=?idZaSM`qr_P^vw)i5yPM5_H!FkP&YgE#m!1VxQyhD9U~FbT-(0@Fi9*Eg@tbRw zXLkV3>jbnwKG798hu%NUGicHCi&f^@h*>>~?Raz+`g)g*hQmx38GngMxM3qk86qV- zR+(q%!Odc*TU{>0X5MuznJFh4_!H?TtnL@5TB5Pka8Km9e>_Z=WwI=qhP=SKi zD45oyU8h_^G^nR-a%noJzjeW z$X{3r;Ln`FPWtETU2E&&W*_C}aQra!XMn3%Q-0(304o&h#8Y#4WEsEWD__eZ#B%j3 z`UM&lIb+MEae4O@duGh-N1Cfy=ZRx$!q7~l>DZnt2<{`rym9zT8Z=NM^e)~F;NBh& z2QO>^a`-8yjeCiV%ME6}u(&%#!2mx%z`x~so*Dvo>xeg`eNpHo-JkeOPK-|WabBBszK_A z8uMd`ndLFM=S+e3?xmro zSC_wb@rK7goPa+uIyQBBP$CT!USHtl!BtVyz3|clKnzr>S}(~&$f0{w@I;kJl(!Zp ze;8f_Ea68LnvJgtEiGFw@?^D2R7LSo82kkq`VFtG$7)k<+x$>YFNLsH)x+(=T4#-7 z6|%xCVcKKMWJY=Ps%*71bamE9Ro>F1Fx8o(Qh8ewLsD-SfvQ6}Qv~QOqfLzw%-_+B zk)>iZ_a0VyKT)K}+o`H)6JqF&yrl$tP%(9V0ZUHR6i{fgLY99E=8Mep2j&`SWkx2koruUzH$J=mgir=t!;# z_&o>n;Zq<1^tJ2!3;wOxFN*$f`xU^-4*EbR=x)%-q_MM4RCPajl_UZuvNu^O{-W8yN~ z=tOyM3g`*4W&1Ij`w9bBeLp}9_?<$&^N_O2_aF3q*BEboHs=lSo-vxY5R+>O7XFtP09K9zqN)Yp5r6S@lDsKd!(SjBftvi zJ006U%han2bUn9$&+C2|KjEq1{6?B7O00LTv(X!!&{o@eI zALo0mR&ZAj|I3zYPJPYVLKaidJDu{(DX9_xu7=BJmND9EO?LQC!)VdVpwo(+Y@!ou zn1{$>mUr6hN_WiHMea9ZP`YMS-pL|h_fHZ3%P(Xs2LDct|KC{MTr0%?uC1*t;(z}v z@&6}y2|R`Ha4MJ{V#h*#=DNFRu82XN(2a%gYPrw!2+BFD!QT4{7Sd$iYk3M0fE-EA zu2Vpq+<4@5D=l-ITBT<_D4`?UB7~WvfOf;yEr6LJ4$a+@vcu)pG;ScLz`p5Q29dC! zh;>{@N?zq-T2ewQD<^efSV;HqB1#rfZ63A0-9PEHPTm}KPCEz9y_cPK z9zD~0#g9dgEyW-xv$;mAR+Eyga_x>K=|<`rYM5OwI^?+T+<=}5B*po0LOrY)`F=0$ zAZhPML1s6G;oaF(_`*wb+lNwsg0yvWGXVr+Vwsj~JStQ&NpY4fE8DE={F2Um9ErRE zdR4XC;gp2hJP^r5?wJ-vqY0KdLDOpfXQk$>d{uzYN5vP*D%0FT`&#IKGwFYrohG$v zf7A58wav|)y#BYjy}da9`4`Lo`*7KFi}`_z332&}fl0ZJN+qhgi6o!4NfWhSX&L=R za;3p2!?G`F9bl!nsKo$)YTyKsv+CeZJ(OKgZY0xUBASHP=w=cH`)J$Cs)2dC$|&Fn zgrYfQVh#D|d7KDD6mDEmEoRBsV!FX%7{o2Q`r74*o^XiOLL*cS9tuw(%k$**dT3bI zU1Ff4WZes=3Tm7*T$BY}%4EJ2(>V%z=)VyOrx=9SL3Y9HT>s)QL1DkB7idc&f%aEk z1@9>TC!Cp>G5`DO*2eZ`A^-d4+WJEN{|V0j&U;3dmFKa`KVc^sc}eCw{3kLBlvAIU zRiOOr4(!=-Q4#i|I=VCP+`(!p0-Vcj_DRV&3hIY)t5qKXu&ACW1(P2 z($9-n3lrD`Y_ZtaGsstUFj(UzOOHzyG-}TH86=oiT8Noiu2tBaB;ylMkXvyWC#p?m zmld)lU#^mC$Ln_eXmS%;k$mkG7H&h#>r#!qt1N%*0_& zWfjdt4^^~*vb7qwrsI@ecqs+7vH$zi?EhxTqr^^>mF2HtWe)Yj{!_{}yA(S`#XeH%7-m8gdXP!#B+;bqBhSd^ zH6Bb;$KRRqU)JC6*~M}sR$*YkS8HmO@B|-MMaOJ%~FTIGK!D* ztQmR+gX$#lf-J2_=}d@tBVVu`kQLryd-`i@yN87WX8WF^hFsJ9jkVRL{!TMK$~zkM zwKhM#!c?Pym7|NzYA;BKp*KG8bj4F(TYD+c?StSAizVr?D02>4fW1NSMwIONX*UT( zL|wjj^-`1{Uc`~l&dH^!0t-~D=r6fmNi8vNA#0!uqBI^n$KzaSZW8oYta>vV3zc-) zifPM?+c2IN8MniHFUB>`Qk0&5GK^+{pA>t-rm}T`*ll2Y+6Mi3dmh zwSSg^f?BLBUVH{~W~@^=V7MoW>+$B=Qx!fY?pngIR#1^~^8u1kK`jL0hq8{@wnKne z4ELUWzo8wf&Mtz~g~c%$iyel09w!~IdjY`FbN%E63trOCc2NH&94)p-i~Zjm z`@iyApJ(0wZSHJr7UO@{7x5o|!uSv6p$jm12$r*@s+>8&b_U<9c zUZI`&Q`;4qn3#?(houSm{k}VOE_M(`n z$d;K%zOW||nw~T$*TD306SMHlnSW>gv^#2&%QA=DCTGa)QiUjrzeT=}Ig)qWE@y|V zI&YSTS#t!GrUo#GIR9>*gZop@>(6@x|86JmwfUZQK0ePaQ%|+ee;&Q`7`py;uCvs) zJ`6qc8R**|WuE5*^NdH4XFq{#KO!s+*>zNb!Xhj<@Ns; z{@^?SJwA-Pm-nS(PSpR`HrLnk_y4SItgSEN{~qM?qVsCWA)3vAcrLPRnC>?kh zY>>L3k~Wqw+Q9FoyF{3+=X{B#lOj%a!(DZsFzWZt&R_%KR9D?Kcbz}#^700V z-F@sn7Ea#b=qwDnr|IbYJh*YHUp;fyp{4bj^B?E2yUJ_Bns&~w38IeHcKC}d9>gi@ zX@ftyPV2;#X$Csuz^Sgeo3b5vH^jzh9l=*?Yfo17x3KA{V;+C(W_{y{0N>K=|M!)% zR&xMc=vlN->_IE_zE(9{Ms#fH3A2IH2ArgQ__U0>avsol+DDS`}Jo~1+f z7{;+~0$4UDt77WTyt6pjiXw&2>#F5-I*z&*3G78n%dlSQbT*6DL4@O}YAZL~pi2Q6KjN<+25X}v^sFyYc9GXa23vW^REq&*sSv&Uy~JmotrccQpYL-o%|H(IX`S5EpN3vU57*b9s7cH(!?b>GAoFXyua;!!Nn4e3 z>oV1SjCD7Ry87yiLcJ);aV;>3R)}3rR|L_z-s;(iWzk|KP?8inmZFxLURRH$XoVLb zUFD1PiUYVI9tp8VLxXM#eHcQHIAo0@PnKY0Oz(EqxM0kWjj0x$-aX<87iyEYLz6&7TEWPlP)2j^* z{D!SKI(M1N5;nM}S8IaKa%H<=JnGfOe4yg8S_TCx3N0oybi&ETTG=9Mm1qo)Vm$@j zDaC2bq3+#vmP4!*HbE4e0^$SYcPO#xK5;j(#Fkz^FOs+QyQ2hylB%o^iLEpkbhD6S zaa&D$<|lv|FRU|@3};|_2>1CrCYrM*6sPjBz`C8e#1g?H{Br!?`+KHW>e9vcvH_3?+WPrJWd84U85xy;k;bS zv7!M?I!+T{taxDZxXGStY9}+5A%VCz?flCWa7H(4V#r>xSyqk;ZCIBCBFQb%so`&) zqrDMsMOZi6LZo;)H;PrBEiFR+dM1+vgM!#gVAlK;sw&FSUuH?5Em@rKJ@x)(5EjN7 zcmtp}@?LmhN>U0pl6OO3QSz-4k=>~o3kRZO1)R#C^5#iq0vGW*0rlJGfa-J zl^|8_fY;PwKMT*4Foa%pP3_RiQS9XxxHYzTZRD1&Q?@FyU6pJ#7Q2imT)e4Op$|H= z1m6H&Ez0Xk2R2sZHGNxR>_-WiTdIl>8|&_q!VA}g=!FKA-c)HYSV8RCH_MSgIcQ-F9^>< z@4cVYV-}!iC@!mdLnFL|p=vzTgt}%%AyJM`HpS?Yc+_Wb$?@km$g4{2lNyjV14`%7 zG#)|O0MXh^;-$cOTcw=0mD^DiId4?}!IUnt>6L8p@@nH!PPNc(~ZlicJmU-8}eteNofTG*uCR-*ZDWxZ37CW$r@@bXBp~1am@tfRJ29{pGY@R$n zJbHD!gm!|#_GZ23CD%bzUq=y(+_$vU?!4|Cv^xi_{SForD)Q3O-kXE=%TC+*Y5(L~ z>6nLT!8pTWHlZIP z*)731v|yYy(WbL_@Cv5YLSDC=UQ6@$IL-g~2isqkRb=P6%qi2ycKfNjm&s$z;9;SX z-sv1szS}!axN+(W=Gx@`TTZjt=zTJ>I6^iF!QwQTInjJzcbMb*m|=OeY*KxB$}C46 z^U=ngDB~MH>MfM=zl;7K24@Ytijon2!@n8E^(>|DB!9jfMXI5T8FP{1{DTccWIp(9g-mrakF_``yT({O#Q=S%p8V*h=fBrAB77_TFsm zm1>O|{Ml?Ug+D1ZIBW@MHMo09YHKfkXui1FGa+S-duHSMmVK~G|P-bgFnOWK)r2xu2nNVgE{%jKJHd^JSw&BkmcHVc&0pgqp03{|sFPcA?CAo~Ee${j<2%w`KlYk3N=));#qLD*OK^DzI zbho0nEYE!}ns3amiQjU-D=BdM`Pt|cNddd}&F%;AXFyXG&Lk)T_!Ch13mvI1ny<`q zeSo9iVgRc(gw{*|?n!%6U0om+>qhPALWIPU4XIF{L_llOln`f{>>+Evo1u4(Q)+=A z$xUie#1_|z7tNn?2xWgtWb}o;N)!+}fC_iZC=BRw?_Fp5RB%$FN?esVw5*6_Q)tv%YfaH;@XvJ_-5wUu zT%bFy5$T-*z5R{1i~9Z#1MK7IUCLGYcX!?Q&368CXM3}7{&RC}vH$xN`#-U5tDI71 z7j~N&?D-P^_fdd%4^;3v=KX^gr=6pt!z1|pI`lG(Ai<5HANt)4z63Gu8+it2_{JQ@ z`qgOgP`kM8h)&=qV|d#4v4U{{n{0qCcd5{6X!#7jpy4T@ovMB5BiDuwCrkZKPUt2C1cwC-628VCW_JXy@I}KkacLNe3 zW<6jy&R0+J$#{2m*BJ_!aqy^!AykFn6`Ou6TX21K$2Ap@}0 zBqUT|E(XTiX-snG+Djrat@#8L0s>273O4zc3gtO;&<X@q6gja`6EYN9^hS!p2s&}A*JqH!+IsS;Aw@8s=`=pjKRy;)oOIH)w zu7f|8$|OSYf~jR(t!!O&YH1rbscrm`wQFBO^Iy_N{9TKK!ndaIKG|zg%Unz49ioQT zwmesAKi`0~E&v$R?L1x#&1=}z`#quNHCx{)a~%NOA=?{`6H9#0)JZJ&BSR54;v^bRjTT3tmL{%C zCfXKaW<)mYt33p_`F|C-ZDe|MpM|;83#U5Z2gxuR${2)7(mhsh`q1F6Ft_pX{w^ zt*`E>M_`81#pX4V^zSVxe%3lVAAD)JKVF;U|9&P+{Gcty z8YZq_HZ>XC($`+u!@E0TLJXKGt4+nuKgsbvX@wdbK}+`-wUqj7Kyycww@nBx+ByV- zmX7hZ2|;uplp%B^O)j40+jkU z*((Jv`dIvxA48X$`E#T{gcV0yYFdVeLIC;Xqp2Qjx08b@1cp>Fjf#6BY+-BfHi#4b zvb}RtZB~?y*l54kdcSVz-o2LEo6?4~eHR#XJ+*DZFf?FTZRwh7+k_!$eY~BE)X*>l zN0L1Pc!SE5T=9}UQ#4%d-MMq#yqKXkhRz(ecQo{DwiF~e?)sRjoW@QqQf3DXz^%QU z68*lTE76Y)7SnvDTYIKTeGgkD2I{u(r!rV~^6+tp4y7zS_4^(SDDt|2eu>wzr zI8$-^G_Ho0H<&?+Z5*lW%jqq@QjfXat<&s8oed4ggSOg_zff^pZDl^% z&v8(D_AT9u98bVFA8Sn8At7H8&W9UbMd>=4m+BFX%|<;ePY-{5jBZj{jX;0*w=#0 zL?2gsa(chTnQ3Yyu4#9*YxwuNCCg@QR*Wo%sOo$?k*2Lkv>G3iR>NSscs}F2J<-T* z)G^`&)FQDjG`i=6iEpQ40{0^A`yB3zf9KA0#_75^#v3xUF7&kuI zGc}AWrp-s~I}h{8Uim=R_vRbvooSj4eP1&SYg7BK{a)khaYwfZ7xXD~;P#_9q5R<( zKmD<8uCAdSytt`Sk7GWNXFpou;$LAO3K~WpW%~q#r94BhYGfQVJbr4BzJAM$S zZAfnrp7+Ti9?#XT`o$-R@OY{3XI`lwhXc!VTZG4va?~7K13aEj3h;QL^u^cZ0Ukf7 z0FUn`1$Z>oCafc)Jc{ugtz10EnQmMEd4_n7&g}6VtqI{AttrtQ&+jvugX4znc4iLg zc&)JUy)~rcI~CIL{k?^BXs}L?>(D^_;lblNG)U(O?)YCl7UkiCI}H5J9N}@Mfd2W@ zM|c=xm>A~qqk_Bp0AU^)V3T7#F81{JlZOlSFfcbM-Xqii4DKi1!|2-@^r73I2e%3O zFuEv-{K!R?JXGX|0ottLA6I6S%Et}=Fd($XQRr^X>)XUp7+qKcL2_Xx^9O=VA4M+K z=%yq%Bo}`2pur)fi&Zf~TV9Az-sP%^pEHP$tj_49yW#4|l=n@Uy=^-XMkYBd$C&WYpU|O8X zyS5SG^6BGDjLE)xu*pyh35oAD*rcrGl3<> zUmAgOqp|t*XBL67_a|Z8JYdCeVEK z*^&UObWXX1Ub=Vc`-?VH-negeA5|aS?W^h4`?%9Kl6t8&MS(Tl?q7VJ=x5|v{KPGit)zhxHxq<7q`P<*!`6LJ;bHOw|O)7?{Yc`_hr41|<@z9{}c zKcR*1uUay3>1%7=(p>5@cy>NpE>i~Kk+C#cGnR5sx~TGMr(+mIZHYr|Nl6P0wX)8< z8Ayw5yz5L#HCimc)v_iQEh!zf%9Dr|5!2X{v$$`zGn9U)!n;?Y4pC{k#A(QUHRSmr zik)9^H%swNwl|>;nM8_jZlouo_%0Dn23ub(TPipPd(^V$?<|{^hCI1hY^!1B-&8E4 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 literal 0 HcmV?d00001 diff --git a/sample/expo/PushNotificationsService.tsx b/sample/expo/PushNotificationsService.tsx index ff1ba2a..06d0ae7 100644 --- a/sample/expo/PushNotificationsService.tsx +++ b/sample/expo/PushNotificationsService.tsx @@ -67,6 +67,8 @@ Notifications.setNotificationHandler({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: true, + shouldShowBanner: true, + shouldShowList: true, }; }, }); diff --git a/sample/expo/app.json b/sample/expo/app.json index d33b3cd..0f6e793 100644 --- a/sample/expo/app.json +++ b/sample/expo/app.json @@ -46,6 +46,19 @@ [ "../../app.plugin.js" ], + [ + "expo-image-picker", + { + "photosPermission": "App needs access to your photos." + } + ], + [ + "expo-camera", + { + "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera", + "recordAudioAndroid": false + } + ], [ "expo-notifications", { @@ -63,7 +76,8 @@ "use_modular_headers!": true } } - ] + ], + "expo-asset" ], "extra": { "eas": { @@ -71,4 +85,4 @@ } } } -} +} \ No newline at end of file diff --git a/sample/expo/components/TouchableOpacityButton.tsx b/sample/expo/components/TouchableOpacityButton.tsx index ca02b39..6277fdc 100644 --- a/sample/expo/components/TouchableOpacityButton.tsx +++ b/sample/expo/components/TouchableOpacityButton.tsx @@ -5,13 +5,15 @@ import { StyleSheet, type TextStyle, type ViewStyle, + StyleProp, } from 'react-native'; interface TouchableOpacityProps { onPress: () => void; buttonText: string; - buttonStyle?: ViewStyle; - textStyle?: TextStyle; + buttonStyle?: StyleProp; + textStyle?: StyleProp; + disabled?: boolean; } const TouchableOpacityButton: React.FC = ({ @@ -19,9 +21,14 @@ const TouchableOpacityButton: React.FC = ({ buttonText, buttonStyle, textStyle, + disabled = false, }) => { return ( - + {buttonText} ); diff --git a/sample/expo/navigator/Navigator.tsx b/sample/expo/navigator/Navigator.tsx index 466764b..1298f32 100644 --- a/sample/expo/navigator/Navigator.tsx +++ b/sample/expo/navigator/Navigator.tsx @@ -9,6 +9,8 @@ import DelayedScreen from '../screens/DelayScreen'; import WebViewScreen from '../screens/WebViewScreen'; import FlatListScreen from '../screens/FlatListScreen'; import { TouchableOpacity, StyleSheet, Text } from 'react-native'; +import CameraScreen from '../screens/CameraScreen'; +import ImageUploadScreen from '../screens/ImageUpload'; export type RootStackParamList = { Home: undefined; @@ -19,6 +21,8 @@ export type RootStackParamList = { DelayScreen: undefined; WebViewScreen: undefined; FlatListScreen: undefined; + ImageUploadScreen: undefined; + CameraScreen: undefined; }; const Stack = createStackNavigator(); @@ -60,6 +64,16 @@ const screens = [ component: FlatListScreen, title: 'Large Scrollable List', }, + { + name: 'CameraScreen', + component: CameraScreen, + title: 'Camera', + }, + { + name: 'ImageUploadScreen', + component: ImageUploadScreen, + title: 'Gallery Upload', + }, ] as const; const createScreen = ( diff --git a/sample/expo/package.json b/sample/expo/package.json index 3bbe00b..f307c36 100644 --- a/sample/expo/package.json +++ b/sample/expo/package.json @@ -20,15 +20,19 @@ "@react-navigation/native-stack": "^7.3.14", "@react-navigation/stack": "^7.1.2", "expo": "^53.0.0", + "expo-asset": "~11.1.7", "expo-build-properties": "~0.14.8", + "expo-camera": "~16.1.11", + "expo-file-system": "~18.1.11", "expo-firebase-messaging": "^2.0.0", + "expo-image-picker": "~16.1.4", "expo-notifications": "~0.31.4", "expo-system-ui": "~5.0.11", "react": "19.0.0", "react-native": "0.79.5", "react-native-device-info": "^14.0.2", "react-native-gesture-handler": "~2.24.0", - "react-native-safe-area-context": "5.4.0", + "react-native-safe-area-context": "^5.6.2", "react-native-screens": "~4.11.1", "react-native-webview": "13.13.5" }, diff --git a/sample/expo/screens/CameraScreen.tsx b/sample/expo/screens/CameraScreen.tsx new file mode 100644 index 0000000..be3d942 --- /dev/null +++ b/sample/expo/screens/CameraScreen.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { View, Text, Image, StyleSheet, Alert } from 'react-native'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import { CameraType } from 'expo-image-picker'; + +const CameraScreen: React.FC = () => { + const [capturedImage, setCapturedImage] = React.useState(null); + const [permission, requestPermission] = useCameraPermissions(); + const cameraRef = React.useRef(null); + const [isCameraReady, setIsCameraReady] = React.useState(false); + + const takePicture = async () => { + if (cameraRef.current && isCameraReady) { + try { + const photo = await cameraRef.current.takePictureAsync({ + quality: 0.8, + base64: false, + exif: false, + }); + if (photo?.uri) { + setCapturedImage(photo.uri); + console.log('Image captured:', photo.uri); + } + } catch (error) { + Alert.alert('Error', 'Could not take photo'); + console.error(error); + } + } + }; + + if (!permission) { + return ( + + Checking permissions... + + ); + } + + return ( + + + {!permission.granted ? ( + + + Camera permission is required. + + + + ) : ( + + setIsCameraReady(true)} + style={styles.camera} + facing={CameraType.back} + ref={cameraRef} + /> + + + )} + + + + {capturedImage ? ( + <> + Captured Image Preview + + setCapturedImage(null)} + buttonStyle={[styles.button, styles.clearButton]} + textStyle={styles.buttonText} + /> + + ) : ( + + Your captured photo will appear here. + + )} + + + ); +}; + +export default CameraScreen; + +const styles = StyleSheet.create({ + button: { + backgroundColor: '#D3D3D3', + padding: 12, + borderRadius: 12, + marginVertical: 4, + alignItems: 'flex-start', + paddingBottom: 8, + }, + buttonText: { + margin: 'auto', + color: '#000000', + fontSize: 16, + paddingLeft: 8, + }, + container: { + flex: 1, + backgroundColor: '#fff', + paddingHorizontal: 10, + }, + topSection: { + flex: 1, + alignItems: 'center', + width: '100%', + marginTop: 20, + }, + cameraWrapper: { + width: '100%', + height: '80%', + overflow: 'hidden', + borderRadius: 15, + }, + camera: { + flex: 1, + height: '100%', + }, + permissionWarning: { + backgroundColor: '#FFF3CD', + padding: 8, + borderRadius: 8, + borderWidth: 1, + borderColor: '#FFE69C', + width: '100%', + }, + permissionWarningText: { + fontSize: 16, + fontWeight: '600', + color: '#856404', + textAlign: 'center', + marginBottom: 10, + }, + cameraButton: { + marginTop: 10, + backgroundColor: '#0091ff', + borderRadius: 10, + width: '100%', + }, + clearButton: { + backgroundColor: '#fc655d', + borderRadius: 10, + width: '100%', + }, + bottomSection: { + width: '100%', + flex: 1, + alignItems: 'center', + justifyContent: 'flex-start', + }, + label: { + fontSize: 20, + fontWeight: '600', + marginBottom: 10, + color: '#333', + textAlign: 'center', + }, + image: { + width: '100%', + height: 200, + objectFit: 'contain', + borderRadius: 10, + borderWidth: 2, + borderColor: '#007AFF', + }, + placeholderText: { + fontSize: 14, + color: '#666', + textAlign: 'center', + fontStyle: 'italic', + }, +}); diff --git a/sample/expo/screens/FlatListScreen.tsx b/sample/expo/screens/FlatListScreen.tsx index c6f810b..30bc72b 100644 --- a/sample/expo/screens/FlatListScreen.tsx +++ b/sample/expo/screens/FlatListScreen.tsx @@ -9,7 +9,7 @@ const DATA = Array.from({ length: 100 }, (_, i) => ({ const refs = DATA.map(() => React.createRef()); -const CardView = React.forwardRef(({ title }: { title: string }, ref) => ( +const CardView = React.forwardRef(({ title }, ref) => ( {title} diff --git a/sample/expo/screens/ImageUpload.tsx b/sample/expo/screens/ImageUpload.tsx new file mode 100644 index 0000000..d12d4af --- /dev/null +++ b/sample/expo/screens/ImageUpload.tsx @@ -0,0 +1,244 @@ +import React from 'react'; +import { + View, + Text, + Image, + StyleSheet, + Alert, + Dimensions, + FlatList, +} from 'react-native'; +import * as ImagePicker from 'expo-image-picker'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; + +const { width } = Dimensions.get('window'); +const imageSize = (width - 60) / 3; + +const RenderImageItem = ({ uri, index }: { uri: string; index: number }) => ( + + + {index + 1} + +); + +const EmptyState = () => ( + + No images selected yet. + +); + +const ImageUploadScreen: React.FC = () => { + const [selectedImages, setSelectedImages] = React.useState< + ImagePicker.ImagePickerAsset[] + >([]); + const [status, requestPermission] = ImagePicker.useMediaLibraryPermissions(); + const [pickerActive, setPickerActive] = React.useState(false); + + if (status === null) { + return ( + + Checking permissions... + + ); + } + + const permissionDenied = + status.status === ImagePicker.PermissionStatus.DENIED; + + const handleSelectImages = async () => { + setPickerActive(true); + if (status?.status !== ImagePicker.PermissionStatus.GRANTED) { + const permissionResponse = await requestPermission(); + if (!permissionResponse.granted) { + Alert.alert( + 'Permission Required', + 'Gallery permission is required to select images.' + ); + setPickerActive(false); + return; + } + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: [], + allowsMultipleSelection: true, + quality: 0.8, + selectionLimit: 0, + }); + + if (result.canceled) { + console.log('User cancelled image picker'); + } else { + setSelectedImages(result.assets); + console.log(`Selected ${result.assets.length} images`); + } + + setPickerActive(false); + }; + + return ( + + + {permissionDenied && ( + + + Gallery permission is required to use this feature. + + + Please enable gallery permission in your device settings. + + + )} + + + + {selectedImages.length > 0 && ( + + {selectedImages.length} image + {selectedImages.length !== 1 ? 's' : ''} selected + + )} + + + ( + + )} + keyExtractor={(item, index) => `${item.uri}-${index}`} + style={styles.imageScrollView} + contentContainerStyle={[ + styles.imageScrollContent, + selectedImages.length === 0 && styles.contentCenter, + ]} + numColumns={3} + columnWrapperStyle={ + selectedImages.length > 0 ? styles.imagesGrid : null + } + ListEmptyComponent={EmptyState} + initialNumToRender={10} + maxToRenderPerBatch={10} + /> + + ); +}; + +export default ImageUploadScreen; + +const styles = StyleSheet.create({ + contentCenter: { + flex: 1, + justifyContent: 'center', + }, + container: { + flex: 1, + backgroundColor: '#fff', + }, + buttonText: { + margin: 'auto', + color: '#000000', + fontSize: 16, + paddingLeft: 8, + }, + topSection: { + paddingVertical: 20, + paddingHorizontal: 20, + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + permissionWarning: { + backgroundColor: '#FFF3CD', + padding: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: '#FFE69C', + width: '100%', + }, + permissionWarningText: { + fontSize: 16, + fontWeight: '600', + color: '#856404', + textAlign: 'center', + marginBottom: 5, + }, + permissionWarningSubtext: { + fontSize: 14, + color: '#856404', + textAlign: 'center', + }, + selectButton: { + marginTop: 10, + backgroundColor: '#0091ff', + borderRadius: 10, + width: '100%', + }, + disabledButton: { + backgroundColor: '#CCCCCC', + opacity: 0.6, + }, + countText: { + marginTop: 15, + fontSize: 16, + fontWeight: '600', + color: '#007AFF', + }, + imageScrollView: { + flex: 1, + }, + imageScrollContent: { + padding: 15, + }, + imagesGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-start', + }, + imageWrapper: { + margin: 5, + position: 'relative', + }, + image: { + width: imageSize, + height: imageSize, + borderRadius: 8, + borderWidth: 2, + borderColor: '#007AFF', + }, + imageNumber: { + position: 'absolute', + top: 5, + right: 5, + backgroundColor: 'rgba(0, 122, 255, 0.9)', + color: '#fff', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + fontSize: 12, + fontWeight: 'bold', + }, + placeholderContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 40, + paddingTop: 60, + }, + placeholderText: { + fontSize: 16, + color: '#666', + textAlign: 'center', + lineHeight: 24, + }, +}); diff --git a/sample/expo/screens/PushNotificationsScreen.tsx b/sample/expo/screens/PushNotificationsScreen.tsx index 8f7939b..32cfe8f 100644 --- a/sample/expo/screens/PushNotificationsScreen.tsx +++ b/sample/expo/screens/PushNotificationsScreen.tsx @@ -9,40 +9,28 @@ const PushNotificationsScreen: React.FC = () => { message: string; } | null>(null); - const handleRegister = async () => { - const isRegistered = viewModel.registerDeviceToken(); - if (await isRegistered) { - setShowDialog({ - title: 'Success', - message: 'Successfully registered for push notifications.', - }); - } else { - setShowDialog({ - title: 'Error', - message: 'Could not register the device.', - }); - } + const registerPushNotifications = () => { + viewModel.registerPushNotifications(); + setShowDialog({ + title: 'Success', + message: 'Successfully registered for push notifications!', + }); }; - const handleUnregister = async () => { - const isUnregistered = viewModel.unregisterDevice(); - if (await isUnregistered) { - setShowDialog({ - title: 'Success', - message: 'Successfully unregistered for push notifications.', - }); - } else { - setShowDialog({ - title: 'Error', - message: 'Could not unregister the device.', - }); - return; - } + const unregisterDevice = () => { + viewModel.unregisterDevice(); + setShowDialog({ + title: 'Success', + message: 'Successfully unregistered the device!', + }); }; const buttons = [ - { text: 'Register for push notifications', onPress: handleRegister }, - { text: 'Unregister for push notifications', onPress: handleUnregister }, + { + text: 'Register for push notifications', + onPress: registerPushNotifications, + }, + { text: 'Unregister for push notifications', onPress: unregisterDevice }, ] as const; return ( diff --git a/sample/expo/screens/SessionAnalyticsScreen.tsx b/sample/expo/screens/SessionAnalyticsScreen.tsx index b9f2f67..ab44415 100644 --- a/sample/expo/screens/SessionAnalyticsScreen.tsx +++ b/sample/expo/screens/SessionAnalyticsScreen.tsx @@ -68,6 +68,16 @@ const ListViewButton = [ }, ] as const; +const MediaButtons = [ + { + text: 'Open Camera', + screenname: 'CameraScreen', + }, + { + text: 'Gallery Image Upload', + screenname: 'ImageUploadScreen', + }, +] as const; const SessionAnalyticsScreen = ({ navigation }: { navigation: any }) => { const sensitiveLabelRef = useRef(null); const unsensitiveLabelRef = useRef(null); @@ -130,6 +140,17 @@ const SessionAnalyticsScreen = ({ navigation }: { navigation: any }) => { /> ))} + Media + {MediaButtons.map((button, index) => ( + navigation.navigate(button.screenname)} + buttonText={button.text} + buttonStyle={styles.button} + textStyle={styles.buttonText} + /> + ))} + Timers {Timer.map((button, index) => ( + + + + + NSLocationWhenInUseUsageDescription + NSCameraUsageDescription + This app needs access to your camera to take photos + NSPhotoLibraryUsageDescription + This app needs access to your photo library to select photos + NSPhotoLibraryAddUsageDescription + This app needs permission to save photos to your library UIBackgroundModes remote-notification diff --git a/sample/react-native/ios/Sources/PrivacyInfo.xcprivacy b/sample/react-native/ios/Sources/PrivacyInfo.xcprivacy index 189b631..412881a 100644 --- a/sample/react-native/ios/Sources/PrivacyInfo.xcprivacy +++ b/sample/react-native/ios/Sources/PrivacyInfo.xcprivacy @@ -20,6 +20,7 @@ NSPrivacyAccessedAPITypeReasons C617.1 + 3B52.1 diff --git a/sample/react-native/package.json b/sample/react-native/package.json index fb31339..74c31b5 100644 --- a/sample/react-native/package.json +++ b/sample/react-native/package.json @@ -15,7 +15,9 @@ "@react-native-firebase/installations": "^21.0.0", "@react-native-firebase/messaging": "^21.0.0", "react-native-device-info": "^14.0.1", + "react-native-image-picker": "^8.2.1", "react-native-notifications": "^5.1.0", + "react-native-vision-camera": "^4.7.3", "react-native-webview": "^13.15.0" }, "devDependencies": { diff --git a/sample/react-native/src/App.tsx b/sample/react-native/src/App.tsx index c729d62..e0c476d 100644 --- a/sample/react-native/src/App.tsx +++ b/sample/react-native/src/App.tsx @@ -13,6 +13,8 @@ import HomeScreen from './screens/HomeScreen'; import DelayedScreen from './screens/DelayedScreen'; import WebViewScreen from './screens/WebViewScreen'; import FlatListScreen from './screens/FlatListScreen'; +import ImageUploadScreen from './screens/ImageUploadScreen'; +import CameraScreen from './screens/CameraScreen'; import { commonStyles } from './styles/styles'; export type RootStackParamList = { @@ -24,6 +26,8 @@ export type RootStackParamList = { DelayedScreen: undefined; WebViewScreen: undefined; FlatListScreen: undefined; + ImageUploadScreen: undefined; + CameraScreen: undefined; }; const Stack = createStackNavigator(); @@ -65,6 +69,16 @@ const screens = [ component: FlatListScreen, title: 'Large Scrollable List', }, + { + name: 'CameraScreen', + component: CameraScreen, + title: 'Camera', + }, + { + name: 'ImageUploadScreen', + component: ImageUploadScreen, + title: 'Gallery Upload', + }, ] as const; const createScreen = ( diff --git a/sample/react-native/src/components/TouchableOpacityButton.tsx b/sample/react-native/src/components/TouchableOpacityButton.tsx index edc2c41..172b78e 100644 --- a/sample/react-native/src/components/TouchableOpacityButton.tsx +++ b/sample/react-native/src/components/TouchableOpacityButton.tsx @@ -5,23 +5,26 @@ import { type TextStyle, type ViewStyle, View, + StyleProp, } from 'react-native'; import { commonStyles } from '../styles/styles'; interface TouchableOpacityProps { onPress: () => void; buttonText: string; - buttonStyle?: ViewStyle; - textStyle?: TextStyle; + buttonStyle?: StyleProp; + textStyle?: StyleProp; + disabled?: boolean; } const TouchableOpacityButton = forwardRef( - ({ onPress, buttonText, buttonStyle, textStyle }, ref) => { + ({ onPress, buttonText, buttonStyle, textStyle, disabled = false }, ref) => { return ( {buttonText} diff --git a/sample/react-native/src/screens/CameraScreen.tsx b/sample/react-native/src/screens/CameraScreen.tsx new file mode 100644 index 0000000..92a4a3c --- /dev/null +++ b/sample/react-native/src/screens/CameraScreen.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { View, Text, Image, StyleSheet, Alert, Linking } from 'react-native'; +import { + Camera, + useCameraDevice, + useCameraPermission, +} from 'react-native-vision-camera'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import { commonStyles } from '../styles/styles'; + +const CameraScreen: React.FC = () => { + const cameraRef = React.useRef(null); + const device = useCameraDevice('back'); + const { hasPermission, requestPermission } = useCameraPermission(); + + const [capturedImage, setCapturedImage] = React.useState(null); + const [capturing, setCapturing] = React.useState(false); + + const handleCapture = async () => { + let granted = hasPermission; + + if (!granted) { + granted = await requestPermission(); + } + + if (!granted) { + Alert.alert('Permission Required', 'Camera permission is required.', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Open Settings', onPress: () => Linking.openSettings() }, + ]); + return; + } + + if (!cameraRef.current) return; + + try { + setCapturing(true); + const photo = await cameraRef.current.takePhoto(); + setCapturedImage(`file://${photo.path}`); + } catch (error) { + console.error(error); + Alert.alert('Camera Error', 'Failed to capture image'); + } finally { + setCapturing(false); + } + }; + + if (!device) { + return ( + + Loading camera... + + ); + } + + return ( + + + + + + + + + {capturedImage ? ( + <> + Captured Image + + setCapturedImage(null)} + buttonStyle={[commonStyles.button, styles.clearButton]} + textStyle={[commonStyles.buttonText]} + /> + + ) : ( + No image captured yet. + )} + + + ); +}; + +export default CameraScreen; + +const styles = StyleSheet.create({ + buttonText: { + fontWeight: '400', + margin: 'auto', + }, + container: { + flex: 1, + backgroundColor: '#fff', + }, + cameraSection: { + flex: 1, + backgroundColor: '#000', + }, + bottomSection: { + flex: 1, + padding: 16, + alignItems: 'center', + }, + captureButton: { + backgroundColor: '#0091ff', + borderRadius: 10, + width: '100%', + marginBottom: 12, + }, + clearButton: { + backgroundColor: '#fc655d', + borderRadius: 10, + width: '100%', + marginTop: 10, + }, + disabledButton: { + backgroundColor: '#CCCCCC', + }, + label: { + fontSize: 16, + fontWeight: '600', + marginVertical: 8, + }, + image: { + width: '100%', + height: 200, + borderRadius: 12, + borderWidth: 2, + borderColor: '#007AFF', + }, + placeholderText: { + fontSize: 14, + color: '#666', + fontStyle: 'italic', + marginTop: 20, + }, + center: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/sample/react-native/src/screens/DelayedScreen.tsx b/sample/react-native/src/screens/DelayedScreen.tsx index 932f395..90e142f 100644 --- a/sample/react-native/src/screens/DelayedScreen.tsx +++ b/sample/react-native/src/screens/DelayedScreen.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { View, Text } from 'react-native'; import * as DevRev from '@devrev/sdk-react-native'; import { commonStyles } from '../styles/styles'; diff --git a/sample/react-native/src/screens/FlatListScreen.tsx b/sample/react-native/src/screens/FlatListScreen.tsx index c6f810b..30bc72b 100644 --- a/sample/react-native/src/screens/FlatListScreen.tsx +++ b/sample/react-native/src/screens/FlatListScreen.tsx @@ -9,7 +9,7 @@ const DATA = Array.from({ length: 100 }, (_, i) => ({ const refs = DATA.map(() => React.createRef()); -const CardView = React.forwardRef(({ title }: { title: string }, ref) => ( +const CardView = React.forwardRef(({ title }, ref) => ( {title} diff --git a/sample/react-native/src/screens/HomeScreen.tsx b/sample/react-native/src/screens/HomeScreen.tsx index 6bea2b6..601b941 100644 --- a/sample/react-native/src/screens/HomeScreen.tsx +++ b/sample/react-native/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React from 'react'; import { View, Text, Platform, Animated } from 'react-native'; import TouchableOpacityButton from '../components/TouchableOpacityButton'; import { commonStyles } from '../styles/styles'; diff --git a/sample/react-native/src/screens/ImageUploadScreen.tsx b/sample/react-native/src/screens/ImageUploadScreen.tsx new file mode 100644 index 0000000..1a7becd --- /dev/null +++ b/sample/react-native/src/screens/ImageUploadScreen.tsx @@ -0,0 +1,300 @@ +import React from 'react'; +import { + View, + Text, + Image, + StyleSheet, + Alert, + Dimensions, + Platform, + PermissionsAndroid, + FlatList, +} from 'react-native'; +import { + launchImageLibrary, + ImagePickerResponse, + Asset, +} from 'react-native-image-picker'; +import TouchableOpacityButton from '../components/TouchableOpacityButton'; +import { commonStyles } from '../styles/styles'; + +const { width } = Dimensions.get('window'); +const imageSize = (width - 60) / 3; + +const RenderImageItem = ({ uri, index }: { uri: string; index: number }) => ( + + + {index + 1} + +); + +const EmptyState = () => ( + + No images selected yet. + +); + +const ImageUploadScreen: React.FC = () => { + const [selectedImages, setSelectedImages] = React.useState([]); + const [hasPermission, setHasPermission] = React.useState( + null + ); + const [permissionDenied, setPermissionDenied] = + React.useState(false); + const [pickerActive, setPickerActive] = React.useState(false); + + React.useEffect(() => { + checkMediaPermission(); + }, []); + + const checkMediaPermission = async () => { + if (Platform.OS === 'android') { + try { + const permission = + Platform.Version >= 33 + ? PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES + : PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE; + const granted = await PermissionsAndroid.check(permission); + setHasPermission(granted); + } catch (err) { + console.warn('Error checking media permission:', err); + setHasPermission(false); + } + } else { + setHasPermission(true); + } + }; + + const requestMediaPermission = async (): Promise => { + if (Platform.OS !== 'android') { + return true; + } + + try { + const permission = + Platform.Version >= 33 + ? PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES + : PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE; + const result = await PermissionsAndroid.request(permission, { + title: 'Storage Permission', + message: 'App needs access to your photos.', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + }); + + const isGranted = result === PermissionsAndroid.RESULTS.GRANTED; + setHasPermission(isGranted); + setPermissionDenied(!isGranted); + return isGranted; + } catch (err) { + console.warn('Error requesting storage permission:', err); + setPermissionDenied(true); + return false; + } + }; + + const handleSelectImages = async () => { + setPickerActive(true); + if (Platform.OS === 'android' && !hasPermission) { + const granted = await requestMediaPermission(); + if (!granted) { + Alert.alert( + 'Permission Required', + 'Gallery permission is required to select images.' + ); + setPickerActive(false); + return; + } + } + + launchImageLibrary( + { + mediaType: 'photo', + selectionLimit: 0, + quality: 0.8, + }, + (response: ImagePickerResponse) => { + if (response.didCancel) { + console.log('User cancelled image picker'); + } else if (response.errorCode) { + Alert.alert( + 'Gallery Error', + response.errorMessage || 'Failed to open gallery' + ); + console.error('Gallery error:', response.errorMessage); + } else if (response.assets && response.assets.length > 0) { + setSelectedImages(response.assets); + console.log(`Selected ${response.assets.length} images`); + } + } + ); + setPickerActive(false); + }; + + return ( + + + {Platform.OS === 'android' && permissionDenied && ( + + + Gallery permission is required to use this feature. + + + Please enable gallery permission in your device settings. + + + )} + + + {selectedImages.length > 0 && ( + + {selectedImages.length} image + {selectedImages.length !== 1 ? 's' : ''} selected + + )} + + + ( + + )} + keyExtractor={(item, index) => `${item.uri}-${index}`} + style={styles.imageScrollView} + contentContainerStyle={[ + styles.imageScrollContent, + selectedImages.length === 0 && styles.contentCenter, + ]} + numColumns={3} + columnWrapperStyle={ + selectedImages.length > 0 ? styles.imagesGrid : null + } + ListEmptyComponent={EmptyState} + initialNumToRender={10} + maxToRenderPerBatch={10} + /> + + ); +}; + +export default ImageUploadScreen; + +const styles = StyleSheet.create({ + contentCenter: { + flex: 1, + justifyContent: 'center', + }, + buttonText: { + fontWeight: '400', + margin: 'auto', + }, + container: { + flex: 1, + backgroundColor: '#fff', + }, + topSection: { + paddingVertical: 20, + paddingHorizontal: 20, + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + permissionWarning: { + backgroundColor: '#FFF3CD', + padding: 15, + borderRadius: 8, + marginTop: 10, + marginHorizontal: 20, + borderWidth: 1, + borderColor: '#FFE69C', + width: '100%', + }, + permissionWarningText: { + fontSize: 16, + fontWeight: '600', + color: '#856404', + textAlign: 'center', + marginBottom: 5, + }, + permissionWarningSubtext: { + fontSize: 14, + color: '#856404', + textAlign: 'center', + }, + selectButton: { + marginTop: 10, + backgroundColor: '#409cff', + width: '100%', + textAlign: 'center', + }, + disabledButton: { + backgroundColor: '#CCCCCC', + opacity: 0.6, + }, + countText: { + marginTop: 15, + fontSize: 16, + fontWeight: '600', + color: '#007AFF', + }, + imageScrollView: { + flex: 1, + }, + imageScrollContent: { + padding: 15, + }, + imagesGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-start', + }, + imageWrapper: { + margin: 5, + position: 'relative', + }, + image: { + width: imageSize, + height: imageSize, + borderRadius: 8, + borderWidth: 2, + borderColor: '#007AFF', + }, + imageNumber: { + position: 'absolute', + top: 5, + right: 5, + backgroundColor: 'rgba(0, 122, 255, 0.9)', + color: '#fff', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + fontSize: 12, + fontWeight: 'bold', + }, + placeholderContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 40, + paddingTop: 60, + }, + placeholderText: { + fontSize: 16, + color: '#666', + textAlign: 'center', + lineHeight: 24, + }, +}); diff --git a/sample/react-native/src/screens/SessionAnalyticsScreen.tsx b/sample/react-native/src/screens/SessionAnalyticsScreen.tsx index dc35835..453663a 100644 --- a/sample/react-native/src/screens/SessionAnalyticsScreen.tsx +++ b/sample/react-native/src/screens/SessionAnalyticsScreen.tsx @@ -43,6 +43,17 @@ const TimerButtons = [ }, ] as const; +const MediaButtons = [ + { + text: 'Open Camera', + screenname: 'CameraScreen', + }, + { + text: 'Gallery Image Upload', + screenname: 'ImageUploadScreen', + }, +] as const; + const OnDemandSessionButtons = [ { text: 'Process All On-Demand Sessions', @@ -150,6 +161,17 @@ const SessionAnalyticsScreen: React.FC<{ navigation: any }> = ({ /> ))} + Media + {MediaButtons.map((button, index) => ( + navigation.navigate(button.screenname)} + buttonText={button.text} + buttonStyle={commonStyles.button} + textStyle={commonStyles.buttonText} + /> + ))} + Manual Masking / Unmasking Date: Mon, 27 Apr 2026 21:22:20 +0530 Subject: [PATCH 2/3] Update changelog version from 2.3.0 to 2.3.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a281a4..7855deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2.3.0] - 2026-04-27 +## [2.3.1] - 2026-04-27 ### Fixed - Improved session recording stability and reduced resource usage on both platforms. From f66e1e449a467f3389ab478fbece8bebdbedec79 Mon Sep 17 00:00:00 2001 From: Yug2801 Date: Mon, 27 Apr 2026 21:42:14 +0530 Subject: [PATCH 3/3] Add changelog for version 2.3.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7855deb..2eed3df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed multiple crashes and ANRs during session recording and app termination. - Fixed incorrect session engagement time calculations on Android. +## [2.3.0] - 2026-02-24 + +### Added +- Feature configuration for screen capture, auto-start recording, plug chat theme, and remote config (fresh vs cached/lazy fetch). +- Support for React Native versions >= 0.79. + +### Changed +- Improved frame capture disabled session replays experience. +- [iOS] Session upload now enforces minimum visit duration before uploading. + +### Fixed +- Fixed a few memory leaks and crashes. + ## [2.2.6] - 2026-02-16 ### Fixed