diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.html b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.html
new file mode 100644
index 0000000000..b8b9bfec28
--- /dev/null
+++ b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.html
@@ -0,0 +1,99 @@
+
+
+
+
Opt in to send text receipts
+
This will help you send receipts via text message.
+
+
+
+
+
+
+
+
+
+
+ Mobile Number
+
+
+ {{ mobileNumberInputValue }}
+
+
+
+
+
+
+
+
+
+ Resend code in
+
+ 0:{{ otpTimer | number : '2.0' }}
+
+ ({{ otpAttemptsLeft }} attempts left)
+
+
+
+
+
+ ({{ otpAttemptsLeft }} attempts left)
+
+
+ Resend Code
+ (0 attempts left)
+
+
+
+
+
+
+
+
+
+
+ We have sent you a confirmation message. You can now use text messages to create and submit your next
+ expense!
+
+
+
+
+
+
+
+
+ Continue
+
+
+
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.scss b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.scss
new file mode 100644
index 0000000000..585a4626e8
--- /dev/null
+++ b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.scss
@@ -0,0 +1,420 @@
+@import '../../../../theme/colors.scss';
+
+.opt-in-step {
+ position: relative;
+ height: 100%;
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+ }
+
+ &__primary-cta-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
+
+ &__card-number-input {
+ width: fit-content !important;
+ margin-right: 24px;
+ &::placeholder {
+ word-spacing: 24px;
+ }
+ }
+
+ &__heading {
+ color: $black;
+ font-size: 20px;
+ font-weight: 500;
+ line-height: normal;
+ margin-bottom: 8px;
+ margin-top: 32px;
+ }
+
+ &__sub-heading {
+ color: $dark-grey;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.28;
+ margin-bottom: 32px;
+ }
+
+ &__primary-cta {
+ width: 108px;
+ align-self: flex-end;
+ }
+
+ &__toolbar-title {
+ font-size: 20px;
+ font-weight: 500;
+ color: $black;
+ line-height: normal;
+ }
+
+ &__toolbar-close-btn {
+ position: absolute;
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__content {
+ --padding-start: 16px;
+ --padding-end: 16px;
+ --padding-top: 16px;
+
+ max-height: 50vh;
+ }
+
+ &__input-container {
+ padding: 16px 16px 8px;;
+ border-radius: 8px;
+ border: 1px solid $grey;
+
+ &:focus-within:not(&__error) {
+ border: 1px solid $black-light;
+ }
+ }
+
+ &__input-label {
+ font-size: 12px;
+ color: $black-light;
+ margin-bottom: 6px;
+ }
+
+ &__input-inner-container {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid $grey;
+ padding-bottom: 6px;
+ margin-bottom: 2px;
+
+ &__error {
+ border-bottom: 1px solid $red;
+ }
+
+ &:focus-within:not(&__error) {
+ border-bottom: 1px solid $black-light;
+ }
+ }
+
+ &__card-number-input {
+ border: 0;
+ color: $blue-black;
+ width: 100%;
+ }
+
+ &__input-default-icon {
+ width: 20px;
+ height: 20px;
+ color: $grey-light;
+ }
+
+ &__input-visa-icon,
+ &__input-mastercard-icon {
+ width: 38px;
+ height: 22px;
+ }
+
+ &__input-error-space {
+ width: 0px;
+ height: 16px;
+ float: right;
+ }
+
+ &__input-errors {
+ color: $red;
+ font-size: 12px;
+ line-height: 1.3;
+
+ & > :not(:first-child) {
+ // Only show one error message at a time
+ display: none;
+ }
+ }
+
+ &__view-tnc-btn {
+ width: 100%;
+ background: $pink-gradient;
+ background-clip: text;
+ color: transparent;
+ padding: 12px 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 14px;
+ }
+
+ &__view-tnc-btn-icon {
+ width: 18px;
+ height: 18px;
+ color: $brand-primary;
+ }
+
+ &__tnc {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ padding-top: 8px;
+ font-size: 14px;
+ }
+
+ &__tnc-heading {
+ font-weight: 500;
+ color: $black;
+ }
+
+ &__tnc-list {
+ margin: 0;
+ padding-left: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &__tnc-link {
+ color: $blue-4;
+ text-decoration: none;
+ }
+
+ &__tnc-link-icon {
+ width: 16px;
+ height: 16px;
+ vertical-align: text-bottom;
+ }
+
+ &__footer-toolbar {
+ padding: 16px;
+ }
+
+ &__toolbar {
+ margin-top: calc(env(safe-area-inset-top));
+ border: 0;
+ }
+
+ &__title {
+ margin-right: 32px;
+
+ &__icon {
+ width: 70px;
+ height: 24px;
+ }
+ }
+
+ &__try-ai-text {
+ margin-left: 6px;
+ font-weight: 500;
+ }
+
+ &__description {
+ font-size: 20px;
+ font-weight: 500;
+ margin-bottom: 48px;
+ line-height: 1.5;
+ }
+
+ &__sparkle-icon {
+ width: 21px;
+ height: 21px;
+ }
+
+ &__edit-icon {
+ color: $brand-primary;
+ margin-left: 4px;
+ margin-bottom: -2px;
+ }
+
+ &__mobile-input-container {
+ &__label {
+ margin-right: 8px;
+ color: $black-light;
+ line-height: 1.3;
+ font-weight: 400;
+ font-size: 12px;
+ }
+
+ &__mandatory {
+ color: $red;
+ }
+
+ &__input::placeholder {
+ font-size: 12px;
+ font-weight: 400;
+ }
+
+ &__input {
+ border: 0;
+ border-radius: 0;
+ font-weight: 500;
+ line-height: 1.3;
+ color: $blue-black;
+ width: 100%;
+ padding: 6px 0;
+ border-bottom: 1px solid $grey-lighter;
+ font-size: 18px;
+
+ &__error {
+ border-bottom: 1px solid $red;
+ }
+ }
+
+ &__input:focus {
+ border-bottom: 1px solid $blue-black;
+ }
+
+ &__error {
+ color: $red;
+ font-size: 12px;
+ }
+ }
+
+ &__primary-cta {
+ margin: 16px auto;
+ width: 90%;
+
+ .mat-button-base {
+ width: 100%;
+ font-weight: 700;
+ min-height: 47px;
+ }
+ }
+
+ &__otp-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 32px;
+
+ &__label {
+ margin: 0 8px 0 0;
+ color: $black-light;
+ line-height: 1.3;
+ font-weight: 400;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ &__attempts {
+ font-size: 12px;
+ margin-left: 8px;
+ }
+
+ &__resend {
+ color: $brand-primary;
+ font-size: 14px;
+ font-weight: 500;
+ background: none;
+
+ &__disabled {
+ @extend .opt-in-step__otp-container__label__resend;
+ opacity: 0.6;
+ }
+ }
+
+ &__otp-timer {
+ font-size: 14px;
+
+ &__timer {
+ color: $brand-primary;
+ }
+ }
+ }
+
+ &__mandatory {
+ color: $red;
+ }
+
+ &__input {
+ border: 0;
+ border-radius: 0;
+ font-weight: 400;
+ line-height: 1.3;
+ color: $blue-black;
+ width: 100%;
+ padding: 8px 0;
+ border-bottom: 1px solid $grey-lighter;
+
+ &__error {
+ border-bottom: 1px solid $red;
+ }
+ }
+
+ &__input:focus {
+ border-bottom: 1px solid $blue-black;
+ }
+
+ &__error {
+ color: $red;
+ font-size: 12px;
+ }
+
+ &__info-box {
+ margin-bottom: 24px;
+ }
+ }
+
+ &__send-code-btn {
+ background: linear-gradient(162.38deg, #ff3366 3.01%, #fe5196 111.5%);
+ &__icon {
+ width: 12px;
+ height: 14px;
+ margin-left: 6px;
+ }
+ }
+
+ &__success {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 90%;
+ align-items: center;
+ margin-top: 24px;
+
+ &__image-container {
+ width: 80px;
+ height: 80px;
+ margin-bottom: 24px;
+ color: $green;
+ }
+
+ &__header {
+ font-weight: 600;
+ font-size: 24px;
+ }
+
+ &__description {
+ margin-top: 16px;
+ width: 95%;
+ text-align: center;
+ padding: 0 24px;
+ }
+
+ &__footer {
+ padding: 14px 16px;
+ gap: 12px;
+
+ &__primary-cta-text,
+ &__secondary-cta-text {
+ font-weight: 500;
+ font-size: 14px;
+ }
+ }
+
+ &__help-article-icon {
+ margin: 4px 0px 0px 6px;
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ &__footer {
+ margin-bottom: calc(env(safe-area-inset-bottom));
+ }
+}
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.spec.ts b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.spec.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.ts b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.ts
new file mode 100644
index 0000000000..28bd3e9aa1
--- /dev/null
+++ b/src/app/fyle/spender-onboarding/spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component.ts
@@ -0,0 +1,305 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import {
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ OnChanges,
+ OnInit,
+ Output,
+ SimpleChanges,
+ ViewChild,
+} from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { ModalController } from '@ionic/angular';
+import { NgOtpInputComponent, NgOtpInputConfig } from 'ng-otp-input';
+import { finalize, from, Subscription, switchMap } from 'rxjs';
+import { CardNetworkType } from 'src/app/core/enums/card-network-type';
+import { OptInFlowState } from 'src/app/core/enums/opt-in-flow-state.enum';
+import { ToastType } from 'src/app/core/enums/toast-type.enum';
+import { ExtendedOrgUser } from 'src/app/core/models/extended-org-user.model';
+import { PlatformCorporateCard } from 'src/app/core/models/platform/platform-corporate-card.model';
+import { PopoverCardsList } from 'src/app/core/models/popover-cards-list.model';
+import { AuthService } from 'src/app/core/services/auth.service';
+import { LoaderService } from 'src/app/core/services/loader.service';
+import { MobileNumberVerificationService } from 'src/app/core/services/mobile-number-verification.service';
+import { OrgUserService } from 'src/app/core/services/org-user.service';
+import { SnackbarPropertiesService } from 'src/app/core/services/snackbar-properties.service';
+import { TrackingService } from 'src/app/core/services/tracking.service';
+import { UserEventService } from 'src/app/core/services/user-event.service';
+import { ToastMessageComponent } from 'src/app/shared/components/toast-message/toast-message.component';
+
+@Component({
+ selector: 'app-spender-onboarding-opt-in-step',
+ templateUrl: './spender-onboarding-opt-in-step.component.html',
+ styleUrls: ['./spender-onboarding-opt-in-step.component.scss'],
+})
+export class SpenderOnboardingOptInStepComponent implements OnInit, OnChanges {
+ @ViewChild('mobileInput') mobileInputEl: ElementRef;
+
+ @ViewChild(NgOtpInputComponent, { static: false }) ngOtpInput: NgOtpInputComponent;
+
+ @Input() eou: ExtendedOrgUser;
+
+ @Output() isStepComplete: EventEmitter = new EventEmitter();
+
+ cardForm: FormControl;
+
+ isVisaRTFEnabled = false;
+
+ isMastercardRTFEnabled = false;
+
+ cardType = CardNetworkType;
+
+ enrollableCards: PlatformCorporateCard[];
+
+ cardValuesMap: Record = {};
+
+ rtfCardType: CardNetworkType;
+
+ cardsList: PopoverCardsList = {
+ successfulCards: [],
+ failedCards: [],
+ };
+
+ fg: FormGroup;
+
+ optInFlowState: OptInFlowState = OptInFlowState.MOBILE_INPUT;
+
+ mobileNumberInputValue: string;
+
+ mobileNumberError: string;
+
+ sendCodeLoading = false;
+
+ otpTimer: number;
+
+ showOtpTimer = false;
+
+ otpError: string;
+
+ disableResendOtp = false;
+
+ otpAttemptsLeft: number;
+
+ verifyingOtp = false;
+
+ hardwareBackButtonAction: Subscription;
+
+ otpConfig: NgOtpInputConfig = {
+ allowNumbersOnly: true,
+ length: 6,
+ inputStyles: {
+ width: '48px',
+ height: '48px',
+ boxShadow: '0px 0px 8px 0px rgba(44, 48, 78, 0.1)',
+ border: 'none',
+ },
+ };
+
+ constructor(
+ private fb: FormBuilder,
+ private trackingService: TrackingService,
+ private modalController: ModalController,
+ private orgUserService: OrgUserService,
+ private authService: AuthService,
+ private mobileNumberVerificationService: MobileNumberVerificationService,
+ private loaderService: LoaderService,
+ private matSnackBar: MatSnackBar,
+ private userEventService: UserEventService,
+ private snackbarProperties: SnackbarPropertiesService
+ ) {}
+
+ get OptInFlowState(): typeof OptInFlowState {
+ return OptInFlowState;
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.eou.currentValue !== changes.eou.previousValue) {
+ this.mobileNumberInputValue = this.eou.ou.mobile;
+ }
+ }
+
+ ngOnInit(): void {
+ this.fg = this.fb.group({});
+ this.fg.addControl('mobile_number', this.fb.control('', [Validators.required, Validators.maxLength(10)]));
+ }
+
+ goBack(): void {
+ if (this.optInFlowState === OptInFlowState.OTP_VERIFICATION) {
+ this.trackingService.optInFlowRetry({
+ message: 'EDIT_NUMBER',
+ });
+ this.optInFlowState = OptInFlowState.MOBILE_INPUT;
+ } else if (this.optInFlowState === OptInFlowState.SUCCESS) {
+ this.trackingService.optInFlowSuccess({
+ message: 'SUCCESS',
+ });
+ this.modalController.dismiss({ action: 'SUCCESS' });
+ } else {
+ this.trackingService.skipOptInFlow();
+ this.modalController.dismiss();
+ }
+ }
+
+ validateInput(): void {
+ if (!this.mobileNumberInputValue?.length) {
+ this.mobileNumberError = 'Please enter mobile number';
+ } else if (!this.mobileNumberInputValue.match(/^\+1\d{10}$/)) {
+ this.mobileNumberError = 'Please enter a valid number with +1 country code. Try re-entering your number.';
+ }
+ }
+
+ saveMobileNumber(): void {
+ //If user has not changed the verified mobile number, close the popover
+ if (this.mobileNumberInputValue === this.eou.ou.mobile && this.eou.ou.mobile_verified) {
+ this.modalController.dismiss();
+ } else {
+ this.validateInput();
+ if (!this.mobileNumberError?.length) {
+ this.sendCodeLoading = true;
+
+ const updatedOrgUserDetails = {
+ ...this.eou.ou,
+ mobile: this.mobileNumberInputValue,
+ };
+ this.orgUserService
+ .postOrgUser(updatedOrgUserDetails)
+ .pipe(switchMap(() => this.authService.refreshEou()))
+ .subscribe({
+ complete: () => {
+ this.resendOtp('INITIAL');
+ },
+ error: () => {
+ this.sendCodeLoading = false;
+ },
+ });
+ }
+ }
+ }
+
+ resendOtp(action: 'CLICK' | 'INITIAL'): void {
+ this.sendCodeLoading = true;
+ this.mobileNumberVerificationService.sendOtp().subscribe({
+ next: (otpDetails) => {
+ this.otpAttemptsLeft = otpDetails.attempts_left;
+
+ if (action === 'INITIAL') {
+ this.optInFlowState = OptInFlowState.OTP_VERIFICATION;
+ }
+
+ if (this.otpAttemptsLeft > 0) {
+ if (action === 'CLICK') {
+ this.toastWithoutCTA('Code sent successfully', ToastType.SUCCESS, 'msb-success-with-camera-icon');
+ this.ngOtpInput.setValue('');
+ }
+ this.startTimer();
+ } else {
+ this.toastWithoutCTA(
+ 'You have reached the limit for 6 digit code requests. Try again after 24 hours.',
+ ToastType.FAILURE,
+ 'msb-failure-with-camera-icon'
+ );
+ this.disableResendOtp = true;
+ }
+
+ this.sendCodeLoading = false;
+ },
+ error: (err: HttpErrorResponse) => {
+ if (err.status === 400) {
+ const error = err.error as { message: string };
+ const errorMessage = error.message?.toLowerCase() || '';
+ if (errorMessage.includes('out of attempts') || errorMessage.includes('max send attempts reached')) {
+ this.trackingService.optInFlowError({
+ message: 'OTP_MAX_ATTEMPTS_REACHED',
+ });
+ this.toastWithoutCTA(
+ 'You have reached the limit for 6 digit code requests. Try again after 24 hours.',
+ ToastType.FAILURE,
+ 'msb-failure-with-camera-icon'
+ );
+ this.ngOtpInput?.setValue('');
+ this.disableResendOtp = true;
+ } else if (errorMessage.includes('invalid parameter')) {
+ this.toastWithoutCTA(
+ 'Invalid mobile number. Please try again.',
+ ToastType.FAILURE,
+ 'msb-failure-with-camera-icon'
+ );
+ } else if (errorMessage.includes('expired')) {
+ this.toastWithoutCTA(
+ 'The code has expired. Please request a new one.',
+ ToastType.FAILURE,
+ 'msb-failure-with-camera-icon'
+ );
+ this.ngOtpInput?.setValue('');
+ } else {
+ this.toastWithoutCTA('Code is invalid', ToastType.FAILURE, 'msb-failure-with-camera-icon');
+ this.ngOtpInput?.setValue('');
+ }
+ }
+
+ this.sendCodeLoading = false;
+ },
+ });
+ }
+
+ verifyOtp(otp: string): void {
+ this.verifyingOtp = true;
+ from(this.loaderService.showLoader('Verifying code...'))
+ .pipe(
+ switchMap(() => this.mobileNumberVerificationService.verifyOtp(otp)),
+ switchMap(() => this.authService.refreshEou()),
+ finalize(() => this.loaderService.hideLoader())
+ )
+ .subscribe({
+ complete: () => {
+ this.optInFlowState = OptInFlowState.SUCCESS;
+ this.verifyingOtp = false;
+ this.isStepComplete.emit(true);
+ this.userEventService.clearTaskCache();
+ },
+ error: () => {
+ this.toastWithoutCTA('Code is invalid', ToastType.FAILURE, 'msb-failure-with-camera-icon');
+ this.ngOtpInput.setValue('');
+ this.verifyingOtp = false;
+ },
+ });
+ }
+
+ onOtpChange(otp: string): void {
+ if (otp.length === 6) {
+ this.verifyOtp(otp);
+ }
+ }
+
+ toastWithoutCTA(toastMessage: string, toastType: ToastType, panelClass: string): void {
+ const message = toastMessage;
+
+ this.matSnackBar.openFromComponent(ToastMessageComponent, {
+ ...this.snackbarProperties.setSnackbarProperties(toastType, { message }),
+ panelClass: [panelClass],
+ });
+ this.trackingService.showToastMessage({ ToastContent: message });
+ }
+
+ startTimer(): void {
+ this.otpTimer = 30;
+ this.showOtpTimer = true;
+ const interval = setInterval(() => {
+ this.otpTimer--;
+ if (this.otpTimer === 0) {
+ clearInterval(interval);
+ this.showOtpTimer = false;
+ }
+ }, 1000);
+ }
+
+ onGotItClicked(): void {
+ this.trackingService.optInFlowSuccess({
+ message: 'SUCCESS',
+ });
+ this.modalController.dismiss({ action: 'SUCCESS' });
+ }
+}
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding.module.ts b/src/app/fyle/spender-onboarding/spender-onboarding.module.ts
index 935cc77298..888dfe0ec8 100644
--- a/src/app/fyle/spender-onboarding/spender-onboarding.module.ts
+++ b/src/app/fyle/spender-onboarding/spender-onboarding.module.ts
@@ -7,22 +7,25 @@ import { CommonModule } from '@angular/common';
import { SpenderOnboardingRoutingModule } from './spender-onboarding-routing.module';
import { MatButtonModule } from '@angular/material/button';
import { SpenderOnboardingConnectCardStepComponent } from './spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component';
+import { SpenderOnboardingOptInStepComponent } from './spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component';
+import { NgOtpInputModule } from 'ng-otp-input';
import { NgxMaskModule } from 'ngx-mask';
@NgModule({
imports: [
- SharedModule,
CommonModule,
FormsModule,
+ SharedModule,
IonicModule,
MatButtonModule,
SpenderOnboardingRoutingModule,
FormsModule,
ReactiveFormsModule,
+ NgOtpInputModule,
NgxMaskModule.forRoot({
validation: false,
}),
],
- declarations: [SpenderOnboardingPage, SpenderOnboardingConnectCardStepComponent],
+ declarations: [SpenderOnboardingPage, SpenderOnboardingConnectCardStepComponent, SpenderOnboardingOptInStepComponent],
})
export class SpenderOnboardingPageModule {}
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding.page.html b/src/app/fyle/spender-onboarding/spender-onboarding.page.html
index 3a602ea4c1..5807b7e12d 100644
--- a/src/app/fyle/spender-onboarding/spender-onboarding.page.html
+++ b/src/app/fyle/spender-onboarding/spender-onboarding.page.html
@@ -26,11 +26,17 @@
Skip
-
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding.page.ts b/src/app/fyle/spender-onboarding/spender-onboarding.page.ts
index 6f45327426..6d0b59842c 100644
--- a/src/app/fyle/spender-onboarding/spender-onboarding.page.ts
+++ b/src/app/fyle/spender-onboarding/spender-onboarding.page.ts
@@ -26,6 +26,8 @@ export class SpenderOnboardingPage {
onboardingStep: typeof OnboardingStep = OnboardingStep;
+ eou: ExtendedOrgUser;
+
orgSettings: OrgSettings;
constructor(
@@ -50,6 +52,7 @@ export class SpenderOnboardingPage {
])
),
map(([eou, orgSettings, onboardingStatus, corporateCards]) => {
+ this.eou = eou;
this.userFullName = eou.us.full_name;
this.orgSettings = orgSettings;
const isRtfEnabled =