diff --git a/.github/workflows/newBuild.yml b/.github/workflows/newBuild.yml index a8c6a098..ddc37bbe 100644 --- a/.github/workflows/newBuild.yml +++ b/.github/workflows/newBuild.yml @@ -1,4 +1,4 @@ -name: Create-Apk-QA +name: generate-apk on: workflow_dispatch: @@ -8,23 +8,31 @@ on: required: true type: choice options: - - qa - - dev + - QA + - Prod + flavor: + description: Choose build flavour + required: true + type: choice + options: + - fieldAgents + - callingAgents type: description: Choose build type required: true type: choice options: - - debug - release version_code: description: Enter app version code (example, 292) - required: false + required: true type: string + default: "292" version_name: description: Enter app version name (example, 3.2.1) - required: false + required: true type: string + default: "3.2.1" jobs: generate: runs-on: [ default ] @@ -69,21 +77,21 @@ jobs: run: chmod +x android/gradlew - name: Create local.properties run: cd android && touch local.properties && echo "sdk.dir = /home/USERNAME/Android/Sdk" > local.properties - - name: Assemble with Stacktrace - QA debug - if: ((github.event.inputs.environment == 'qa' || inputs.environment == 'qa') && (github.event.inputs.type == 'debug' || inputs.type == 'debug')) - run: yarn move:qa && yarn debug && cd android && ./gradlew assembleDebug - - name: Assemble with Stacktrace - Dev debug - if: ((github.event.inputs.environment == 'dev' || inputs.environment == 'dev') && (github.event.inputs.type == 'debug' || inputs.type == 'debug')) - run: yarn move:dev && yarn debug && cd android && ./gradlew assembleDebug - - name: Assemble with Stacktrace - QA release - if: ((github.event.inputs.environment == 'qa' || inputs.environment == 'qa') && (github.event.inputs.type == 'release' || inputs.type == 'release')) - run: yarn move:qa && cd android && ./gradlew assembleRelease - - name: Assemble with Stacktrace - Dev release - if: ((github.event.inputs.environment == 'dev' || inputs.environment == 'dev') && (github.event.inputs.type == 'debug' || inputs.type == 'release')) - run: yarn move:dev && cd android && ./gradlew assembleRelease + - name: Assemble with Stacktrace - Field QA release + if: ((github.event.inputs.environment == 'QA' || inputs.environment == 'QA') && (github.event.flavor.type == 'fieldAgents' || inputs.flavor == 'fieldAgents')) + run: yarn move:qa && cd android && ./gradlew assemblefieldAgentsQARelease + - name: Assemble with Stacktrace - Field PROD release + if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod') && (github.event.flavor.type == 'fieldAgents' || inputs.flavor == 'fieldAgents')) + run: yarn move:prod && cd android && ./gradlew assemblefieldAgentsProdRelease + - name: Assemble with Stacktrace - Calling QA release + if: ((github.event.inputs.environment == 'QA' || inputs.environment == 'QA') && (github.event.flavor.type == 'callingAgents' || inputs.flavor == 'callingAgents')) + run: yarn move:qa && cd android && ./gradlew assemblefieldAgentsQARelease + - name: Assemble with Stacktrace - Calling PROD release + if: ((github.event.inputs.environment == 'Prod' || inputs.environment == 'Prod') && (github.event.flavor.type == 'callingAgents' || inputs.flavor == 'callingAgents')) + run: yarn move:prod && cd android && ./gradlew assemblefieldAgentsProdRelease - name: Upload APK as Artifact uses: actions/upload-artifact@v3 with: - name: app-${{ github.event.inputs.type || inputs.type }} - path: android/app/build/outputs/apk/${{ github.event.inputs.type || inputs.type }}/ + name: app-${{ github.event.inputs.type || inputs.type }}-v${{ github.event.inputs.version_code || inputs.version_code }}-name-${{github.event.inputs.version_name || inputs.version_name}} + path: android/app/build/outputs/apk/${{ github.event.inputs.flavor || inputs.flavor }}${{github.event.inputs.environment || inputs.environment}}/${{github.event.inputs.type || inputs.type}} retention-days: 30 diff --git a/android/app/build.gradle b/android/app/build.gradle index a36fe82e..0b4a2be9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -313,7 +313,7 @@ dependencies { implementation "com.github.anrwatchdog:anrwatchdog:1.4.0" - implementation 'com.navi.medici:alfred:v1.0.1' + implementation 'com.navi.medici:alfred:v1.0.2' //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" // From node_modules diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b6c2906d..463e9f52 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ + { + Log.d("Alfred", "setBottomSheetView nativeViewHierarchyManager:" + nativeViewHierarchyManager); + View view = nativeViewHierarchyManager.resolveView(refID); + Log.d("Alfred", "setBottomSheetView view:" + view); + AlfredManager.INSTANCE.setBottomSheetView(view); + }); + } catch (Exception error) { + Log.d("Alfred", "setBottomSheetView error:" + error); + } + } } - return; } + + @ReactMethod + public void clearBottomSheet() { + AlfredManager.INSTANCE.clearBottomSheetView(); + } + private static File convertBase64ToFile(Context context,String base64Data) { try { byte[] decodedBytes = Base64.decode(base64Data, Base64.DEFAULT); @@ -207,50 +229,87 @@ public class DeviceUtilsModule extends ReactContextBaseJavaModule { } } - public boolean isWhatsAppInstalled() { + public ArrayList isWhatsAppInstalled() { PackageManager packageManager = RNContext.getPackageManager(); List packages = packageManager.getInstalledPackages(PackageManager.GET_META_DATA); + ArrayList appsInstalled = new ArrayList(); for (PackageInfo packageInfo : packages) { String packageName = packageInfo.packageName; - if(packageName.equals("com.whatsapp")){ - return true; + if(packageName.equals("com.whatsapp") || packageName.equals("com.whatsapp.w4b")){ + appsInstalled.add(packageName); } } - return false; + return appsInstalled; + } + + public Intent getWhatsappShareIntent(String message, String imageUrl, String mimeType, String packageName) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, message); + + if (imageUrl.equals("")) { + sendIntent.setType("text/plain"); + sendIntent.setPackage(packageName); + } else { + sendIntent.setType(mimeType); + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + imageFile = convertBase64ToFile(getReactApplicationContext(), imageUrl); + Uri fileUri = FileProvider.getUriForFile(getReactApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", new File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + imageFile.getName() + ) + ); + sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); + sendIntent.setPackage(packageName); + } + + return sendIntent; } @ReactMethod public void sendFeedbackToWhatsapp(String message, String imageUrl, String mimeType, Promise promise) { try{ - if(!isWhatsAppInstalled()){ + ArrayList appsInstalled = isWhatsAppInstalled(); + int numberOfAppsInstalled = appsInstalled.size(); + + if(numberOfAppsInstalled == 0){ promise.reject("errorCode", "1"); return; } - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, message); - - if(imageUrl.equals("")) { - sendIntent.setType("text/plain"); - sendIntent.setPackage("com.whatsapp"); - getCurrentActivity().startActivity(sendIntent); - } else { - sendIntent.setType(mimeType); - sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - imageFile = convertBase64ToFile(getReactApplicationContext(), imageUrl); - Uri fileUri = FileProvider.getUriForFile(getReactApplicationContext(), BuildConfig.APPLICATION_ID + ".provider", new File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - imageFile.getName() - ) - ); - sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - sendIntent.setPackage("com.whatsapp"); - getCurrentActivity().startActivityForResult(sendIntent, WHATSAPP_SHARE_REQUEST_CODE); + else if(numberOfAppsInstalled == 1) { + String packageName = appsInstalled.get(0); + Intent sendIntent = getWhatsappShareIntent(message, imageUrl, mimeType, packageName); + if(getCurrentActivity()!=null) { + getCurrentActivity().startActivityForResult(sendIntent, WHATSAPP_SHARE_REQUEST_CODE); + } + promise.resolve(true); + return; + } + + else { + String packageName1 = appsInstalled.get(0); + String packageName2 = appsInstalled.get(1); + //Firing two intents, one for WhatsApp, another for WhatsApp business + Intent sendIntent1 = getWhatsappShareIntent(message, imageUrl, mimeType, packageName1); + Intent sendIntent2 = getWhatsappShareIntent(message, imageUrl, mimeType, packageName2); + ArrayList appIntents = new ArrayList<>(); + appIntents.add(sendIntent1); + appIntents.add(sendIntent2); + + Intent defaultIntent = new Intent(android.content.Intent.ACTION_SEND); + defaultIntent.setType("text/plain"); + defaultIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Sharing to WhatsApp"); + + Intent chooserIntent = Intent.createChooser(defaultIntent, "Share via"); + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, appIntents.toArray(new Parcelable[appIntents.size()])); + if (getCurrentActivity() != null) { + getCurrentActivity().startActivityForResult(chooserIntent, WHATSAPP_SHARE_REQUEST_CODE); + } + promise.resolve(true); + return; } - promise.resolve(true); - return; } catch (Error e){ promise.reject("errorCode","2"); diff --git a/src/action/feedbackActions.ts b/src/action/feedbackActions.ts index 27c0f7be..00f89844 100644 --- a/src/action/feedbackActions.ts +++ b/src/action/feedbackActions.ts @@ -9,6 +9,13 @@ interface IPastFeedbacksPayload { addressReferenceIds?: string; // required to fetch past feedback on addresses } +export interface IFilterPayload { + filters: Array<{ + filterName: string; + selectedValue: string[]; + }>; +} + interface IPastFeedbacksOnAddressesPayload { loanAccountNumber: string; pageNo?: number; @@ -16,12 +23,13 @@ interface IPastFeedbacksOnAddressesPayload { addressReferenceIds?: string; } -export const getPastFeedbacks = (queryParamsPayload: IPastFeedbacksPayload) => { - const url = getApiUrl(ApiKeys.PAST_FEEDBACK); +export const getPastFeedbacks = ( + queryParamsPayload: IPastFeedbacksPayload, + filterPayload: IFilterPayload +) => { + const url = getApiUrl(ApiKeys.PAST_FEEDBACK, {}, queryParamsPayload); return axiosInstance - .get(url, { - params: queryParamsPayload, - }) + .post(url, filterPayload) .then((response) => { if (response?.data) { return { diff --git a/src/assets/icons/FilterIcon.tsx b/src/assets/icons/FilterIcon.tsx index 825c24eb..95b472d0 100644 --- a/src/assets/icons/FilterIcon.tsx +++ b/src/assets/icons/FilterIcon.tsx @@ -1,10 +1,13 @@ import * as React from 'react'; import Svg, { Path } from 'react-native-svg'; -const FilterIcon = () => ( +import { IconProps } from '../../../RN-UI-LIB/src/Icons/types'; +import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; + +const FilterIcon: React.FC = ({ fillColor = COLORS.BACKGROUND.LIGHT }) => ( ); diff --git a/src/common/BottomSheetWrapper.tsx b/src/common/BottomSheetWrapper.tsx index 01d9561d..15ecb14c 100644 --- a/src/common/BottomSheetWrapper.tsx +++ b/src/common/BottomSheetWrapper.tsx @@ -2,24 +2,28 @@ import React from 'react'; import BottomSheet, { BottomSheetProps, } from '../../RN-UI-LIB/src/components/bottom_sheet/BottomSheet'; -import { sendBottomSheetOpenSignal } from '../components/utlis/DeviceUtils'; -import { NativeSyntheticEvent } from 'react-native'; +import { clearBottomSheet, setBottomSheetView } from '../components/utlis/DeviceUtils'; interface IBottomSheetWrapperProps extends BottomSheetProps {} const BottomSheetWrapper: React.FC = (props) => { const { children, onShow, onSwipeDownClose, onClose, ...restProps } = props; const onCloseHandler = () => { - sendBottomSheetOpenSignal(false); + clearBottomSheet(); if (typeof onClose === 'function') onClose(); }; - const onShowHandler = (event: NativeSyntheticEvent) => { - sendBottomSheetOpenSignal(true); - if (typeof onShow === 'function') onShow(event); + const onAnimationEndHandler = (id: number | null) => { + if (!id) return; + setBottomSheetView(id); }; return ( - + {children} ); diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 4f347b30..1441ae6c 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -747,6 +747,7 @@ export const REQUEST_TO_UNBLOCK_FOR_IMPERSONATION = [ getApiUrl(ApiKeys.GET_SIGNED_URL), getApiUrl(ApiKeys.GET_SIGNED_URL_FOR_REPORTEE), getApiUrl(ApiKeys.LOGOUT), + getApiUrl(ApiKeys.PAST_FEEDBACK), ]; export const NAVI_AGENCY_CODE = '1000'; diff --git a/src/common/DropDownWrapper.tsx b/src/common/DropDownWrapper.tsx index e3ba6f4b..97f20582 100644 --- a/src/common/DropDownWrapper.tsx +++ b/src/common/DropDownWrapper.tsx @@ -1,19 +1,27 @@ import React from 'react'; import Dropdown, { IDropdown } from '../../RN-UI-LIB/src/components/dropdown/Dropdown'; -import { sendBottomSheetOpenSignal } from '../components/utlis/DeviceUtils'; +import { + clearBottomSheet, + sendBottomSheetOpenSignal, + setBottomSheetView, +} from '../components/utlis/DeviceUtils'; const DropDownWrapper: React.FC = (props) => { - const { onShow, onClose, children, ...remainingProps } = props; - const onShowHandler = () => { - if (typeof onShow === 'function') onShow(); - sendBottomSheetOpenSignal(true); - }; + const { onShow, onClose, onAnimationEnd, children, ...remainingProps } = props; + const onCloseHandler = () => { if (typeof onClose === 'function') onClose(); - sendBottomSheetOpenSignal(false); + clearBottomSheet(); }; + + const onAnimationEndHandler = (id: number | null) => { + if (!id) return; + console.log('dropdown opened', id); + setBottomSheetView(id); + }; + return ( - + {children} ); diff --git a/src/common/ModalWrapperForAlfredV2.tsx b/src/common/ModalWrapperForAlfredV2.tsx new file mode 100644 index 00000000..4577809f --- /dev/null +++ b/src/common/ModalWrapperForAlfredV2.tsx @@ -0,0 +1,45 @@ +import React, { useEffect } from 'react'; +import { Modal, NativeSyntheticEvent, View, findNodeHandle } from 'react-native'; +import { IModalWrapper } from '../../RN-UI-LIB/src/components/modalWrapper/ModalWrapper'; +import { + clearBottomSheet, + sendBottomSheetOpenSignal, + setBottomSheetView, +} from '../components/utlis/DeviceUtils'; +import { GenericStyles } from '@rn-ui-lib/styles'; + +const ModalWrapperForAlfredV2: React.FC = ({ children, ...props }) => { + const { onRequestClose, onShow, visible } = props; + const modalRef = React.useRef(null); + const lastSent = React.useRef(visible); + const onRequestCloseHandler = (event: NativeSyntheticEvent) => { + if (typeof onRequestClose === 'function') onRequestClose(event); + clearBottomSheet(); + }; + const onShowHandler = (event: NativeSyntheticEvent) => { + if (typeof onShow === 'function') onShow(event); + const nodeId = findNodeHandle(modalRef.current); + lastSent.current = true; + setBottomSheetView(nodeId); + }; + + return ( + + + {children} + + + ); +}; + +export default ModalWrapperForAlfredV2; diff --git a/src/components/filters/FilterOptions.tsx b/src/components/filters/FilterOptions.tsx new file mode 100644 index 00000000..fc0955e2 --- /dev/null +++ b/src/components/filters/FilterOptions.tsx @@ -0,0 +1,122 @@ +import { View } from 'react-native'; +import React from 'react'; +import { IFilters, TFilterOptions } from './Filters'; +import RadioGroup from '../../../RN-UI-LIB/src/components/radio_button/RadioGroup'; +import RNRadioButton from '../../../RN-UI-LIB/src/components/radio_button/RadioButton'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import Checkbox from '../../../RN-UI-LIB/src/components/chechbox/Checkbox'; + +interface IFilterOptions { + filters: IFilters; + selectedFilter: string; + selectedFilterOptions: TFilterOptions; + setSelectedFilterOptions: React.Dispatch>; +} + +enum FilterTypes { + RADIO_GROUP = 'RADIO_GROUP', + CHECKBOX_GROUP = 'CHECKBOX_GROUP', +} + +const FilterOptions: React.FC = ({ + filters, + selectedFilter, + selectedFilterOptions, + setSelectedFilterOptions, +}) => { + if (!selectedFilter || !filters?.[selectedFilter]) { + return null; + } + + const handleSingleOptionChange = (value: string) => { + if (selectedFilter) { + const selectedOption = !!selectedFilterOptions?.[selectedFilter]?.filters?.[value]; + const currentSelectedFilterCount = + selectedFilterOptions?.[selectedFilter]?.selectedFilterCount || 0; + if (selectedOption && currentSelectedFilterCount === 1) { + // delete the selected filter if we are unchecking the only selected option + setSelectedFilterOptions((prev) => { + const updatedOptions = { ...prev }; + delete updatedOptions[selectedFilter]; + return updatedOptions; + }); + return; + } + setSelectedFilterOptions({ + ...selectedFilterOptions, + [selectedFilter]: { + filters: { + [value]: !selectedOption, + }, + selectedFilterCount: selectedOption ? 0 : 1, + }, + }); + } + }; + + const handleMultiOptionChange = (isChecked: boolean, values: string) => { + if (selectedFilter) { + const currentFilters = selectedFilterOptions?.[selectedFilter]?.filters || {}; + const currentSelectedFilterCount = + selectedFilterOptions?.[selectedFilter]?.selectedFilterCount || 0; + if (!isChecked && currentSelectedFilterCount === 1) { + // delete the selected filter if we are unchecking the only selected option + setSelectedFilterOptions((prev) => { + const updatedOptions = { ...prev }; + delete updatedOptions[selectedFilter]; + return updatedOptions; + }); + return; + } + setSelectedFilterOptions({ + ...selectedFilterOptions, + [selectedFilter]: { + filters: { + ...currentFilters, + [values]: isChecked, + }, + selectedFilterCount: isChecked + ? currentSelectedFilterCount + 1 + : currentSelectedFilterCount - 1, + }, + }); + } + }; + + const filter = filters[selectedFilter]; + const selectionType = filter.filterType; + const selectedOptions = selectedFilterOptions[selectedFilter]?.filters || {}; + + if (selectionType === FilterTypes.RADIO_GROUP) { + const selectedOption = Object.keys(selectedOptions)[0]; + return ( + handleSingleOptionChange(value)} + orientation="vertical" + > + {filter.options.map((option) => ( + + ))} + + ); + } else if (selectionType === FilterTypes.CHECKBOX_GROUP) { + return ( + <> + {filter.options.map((option) => ( + + handleMultiOptionChange(value, option.value)} + /> + + ))} + + ); + } + + return null; +}; + +export default FilterOptions; diff --git a/src/components/filters/Filters.tsx b/src/components/filters/Filters.tsx new file mode 100644 index 00000000..892284ee --- /dev/null +++ b/src/components/filters/Filters.tsx @@ -0,0 +1,196 @@ +import React, { useEffect, useState } from 'react'; +import { View, ScrollView, StyleSheet, PixelRatio, Pressable } from 'react-native'; +import { GenericStyles } from '../../../RN-UI-LIB/src/styles'; +import Text from '../../../RN-UI-LIB/src/components/Text'; +import Button from '../../../RN-UI-LIB/src/components/Button'; +import { COLORS } from '../../../RN-UI-LIB/src/styles/colors'; +import { _map } from '../../../RN-UI-LIB/src/utlis/common'; +import FilterOptions from './FilterOptions'; +import NavigationHeader from '../../../RN-UI-LIB/src/components/NavigationHeader'; + +interface FilterOption { + value: string; + label: string; +} + +interface FilterData { + filterType: string; + displayText: string; + options: FilterOption[]; + name: string; +} + +export interface IFilters { + [key: string]: FilterData; +} + +interface IFilterOptions { + filters: Record; + selectedFilterCount: number; +} + +export type TFilterOptions = Record; + +const Filters: React.FC<{ + header: string; + filters: IFilters; + defaultSelectedFilters: TFilterOptions; + closeFilterModal: () => void; + onFilterChange: (selectedFilters: TFilterOptions) => void; +}> = ({ header, filters, defaultSelectedFilters, closeFilterModal, onFilterChange }) => { + // Default select first filter + const [selectedFilter, setSelectedFilter] = useState(Object.keys(filters)[0]); + const [selectedFilterOptions, setSelectedFilterOptions] = + useState(defaultSelectedFilters); + + useEffect(() => { + if (defaultSelectedFilters) { + setSelectedFilterOptions(defaultSelectedFilters); + } + }, [defaultSelectedFilters]); + + const handleFilterChange = (filterName: string) => { + setSelectedFilter(filterName); + }; + + const handleSubmit = () => { + onFilterChange(selectedFilterOptions); + closeFilterModal(); + }; + + const handleClearAll = () => { + onFilterChange({}); + setSelectedFilterOptions({}); + }; + + return ( + + + + + {/* @ts-expect-error */} + {_map(filters, (filterKey) => { + const filter = filters[filterKey]; + const filterName = filter.name; + + return ( + handleFilterChange(filterName)} + style={[ + GenericStyles.row, + GenericStyles.spaceBetween, + GenericStyles.ph12, + { paddingVertical: 8 }, + GenericStyles.br6, + GenericStyles.mb8, + GenericStyles.alignCenter, + { + backgroundColor: + selectedFilter === filterName + ? COLORS.BACKGROUND.BLUE + : COLORS.BACKGROUND.PRIMARY, + }, + ]} + > + + {filter.displayText} + + {selectedFilterOptions[filterName]?.selectedFilterCount ? ( + + + {selectedFilterOptions[filterName]?.selectedFilterCount} + + + ) : ( + + )} + + ); + })} + + + + + + +