TP-61357: Introducing ReactNative | Insurance - Quote page experiments (#10199)

Co-authored-by: Shivam Goyal <shivam.goyal@navi.com>
This commit is contained in:
Raaj Gopal
2024-03-27 20:36:03 +05:30
committed by GitHub
parent 19b8fe49da
commit d27044fd5f
216 changed files with 25611 additions and 72 deletions

View File

@@ -2,9 +2,9 @@ name: Android Build CI
on:
push:
branches: [ master, release-*, development ]
branches: [ master, release-* ]
pull_request:
branches: [ master, release-*, development ]
branches: [ master, release-* ]
merge_group:
concurrency:
@@ -20,7 +20,7 @@ jobs:
output: APK
build-qa-release:
if: github.event_name == 'push' && (github.ref_name == 'master' || startsWith(github.ref_name, 'release-'))
if: github.event_name == 'push' && startsWith(github.ref_name, 'release-')
uses: ./.github/workflows/generate_build.yml
with:
environment: qa
@@ -28,7 +28,7 @@ jobs:
output: APK
generate-apk-diff:
if: github.event_name == 'pull_request' && github.base_ref == 'development'
if: github.event_name == 'pull_request' && github.base_ref == 'master'
uses: ./.github/workflows/generate_apk_diff.yml
needs: build-qa-debug
secrets:
@@ -36,7 +36,7 @@ jobs:
AWS_SECRET_KEY_GITHUB_CACHE: ${{ secrets.AWS_SECRET_KEY_GITHUB_CACHE }}
upload-apk-to-s3:
if: github.event_name == 'push' && github.ref_name == 'development'
if: github.event_name == 'push' && github.ref_name == 'master'
uses: ./.github/workflows/upload_file.yml
needs: build-qa-debug
secrets:

View File

@@ -2,7 +2,7 @@ name: Android Checkstyle CI
on:
pull_request:
branches: [ master, release-*, development ]
branches: [ master, release-* ]
merge_group:
concurrency:
@@ -18,8 +18,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Fetch origin/development
run: git fetch origin development
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.0
- name: Install Node Modules
run: npm install
- name: Set up JDK 17
uses: actions/setup-java@v4
with:

View File

@@ -74,6 +74,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.0
- name: Install Node Modules
run: npm install
- name: Override Version Code
if: github.event_name == 'workflow_dispatch' && inputs.version_code != ''
run: sed -i 's/def VERSION_CODE = [0-9].*/def VERSION_CODE = ${{ inputs.version_code }}/g' app/build.gradle

View File

@@ -25,6 +25,12 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18.18.0
- name: Install Node Modules
run: npm install
- name: Log Build Metadata
run: |
echo "Commit SHA: ${{ github.sha }}"

View File

@@ -3,7 +3,7 @@ name: Security API Diff Monitor
on:
pull_request:
branches:
- development
- master
types: [ opened, edited, synchronize, reopened ]
merge_group:

View File

@@ -6,7 +6,6 @@ on:
branches:
- master
- main
- development
- release-*
schedule:
- cron: '30 4 * * MON'

View File

@@ -23,7 +23,7 @@ jobs:
run: |
echo "ORIGINAL_PR_NUMBER=$(echo "${{ github.event.pull_request.body }}" | grep -o "https://github.com/${{ github.repository }}/pull/[0-9]*" | grep -o "[0-9]*")" >> $GITHUB_ENV
echo "CURRENT_PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
- name: Verification Step 1 - Link to development PR is added in release PR Body
- name: Verification Step 1 - Link to master PR is added in release PR Body
if: always()
run: |
ORIGINAL_PR_URL="https://github.com/${{ github.repository }}/pull/$ORIGINAL_PR_NUMBER"
@@ -33,10 +33,10 @@ jobs:
echo "Pull request body: ${{ github.event.pull_request.body }}"
if [[ "${{ github.event.pull_request.body }}" != *"$ORIGINAL_PR_URL"* ]]; then
echo "Link to original PR raised against development branch not found in current PR body"
echo "Link to original PR raised against master branch not found in current PR body"
exit 1
fi
- name: Verification Step 2 - release PR Title matches development PR Title
- name: Verification Step 2 - release PR Title matches master PR Title
if: always()
run: |
ORIGINAL_PR_TITLE=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$ORIGINAL_PR_NUMBER" | jq -r .title)
@@ -46,53 +46,53 @@ jobs:
echo "Current PR title extracted: ${{ github.event.pull_request.title }}"
if [[ "${{ github.event.pull_request.title }}" != "$ORIGINAL_PR_TITLE" ]]; then
echo "Current PR title does not match original PR title raised against development branch"
echo "Current PR title does not match original PR title raised against master branch"
exit 1
fi
- name: Verification Step 3 - development PR is Merged
- name: Verification Step 3 - master PR is Merged
if: always()
run: |
ORIGINAL_PR_IS_MERGED=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$ORIGINAL_PR_NUMBER" | jq -r '.merged')
BASE_REF_BRANCH=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$ORIGINAL_PR_NUMBER" | jq -r '.base.ref')
if [[ "$ORIGINAL_PR_IS_MERGED" == "true" && "$BASE_REF_BRANCH" == "development" ]]; then
echo "Original PR is merged into development branch"
if [[ "$ORIGINAL_PR_IS_MERGED" == "true" && "$BASE_REF_BRANCH" == "master" ]]; then
echo "Original PR is merged into master branch"
else
echo "Original PR number: $ORIGINAL_PR_NUMBER"
echo "Original PR merge status: $ORIGINAL_PR_IS_MERGED"
echo "Base branch: $BASE_REF_BRANCH"
echo "Status: Fail. Original PR is not merged in development branch."
echo "Status: Fail. Original PR is not merged in master branch."
exit 1
fi
- name: Verification Step 4 - release PR is exactly same as development PR
- name: Verification Step 4 - release PR is exactly same as master PR
if: always()
run: |
# Get the JSON response for the first pull request
response_pr_development=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$ORIGINAL_PR_NUMBER/files")
files_pr_development=$(echo "$response_pr_development" | jq -r '.[].filename')
response_pr_master=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$ORIGINAL_PR_NUMBER/files")
files_pr_master=$(echo "$response_pr_master" | jq -r '.[].filename')
# Get the JSON response for the second pull request
response_pr_release=$(curl -L -H "Authorization: Bearer ${{ secrets.GH_PAT_RO }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$CURRENT_PR_NUMBER/files")
files_pr_release=$(echo "$response_pr_release" | jq -r '.[].filename')
# Compare the lists of changed files
if [ "$files_pr_development" != "$files_pr_release" ]; then
if [ "$files_pr_master" != "$files_pr_release" ]; then
echo "Pull requests have different sets of changed files."
exit 1
fi
# Loop through each file and compare added and deleted lines
for file in $details_pr_development; do
details_pr_development=($(echo "$response_pr_development" | jq -r --arg file "$file" '.[] | select(.filename == $file) | .additions, .deletions'))
for file in $details_pr_master; do
details_pr_master=($(echo "$response_pr_master" | jq -r --arg file "$file" '.[] | select(.filename == $file) | .additions, .deletions'))
details_pr_release=($(echo "$response_pr_release" | jq -r --arg file "$file" '.[] | select(.filename == $file) | .additions, .deletions'))
added_lines_pr_development="${details_pr_development[0]}"
deleted_lines_pr_development="${details_pr_development[1]}"
added_lines_pr_master="${details_pr_master[0]}"
deleted_lines_pr_master="${details_pr_master[1]}"
added_lines_pr_release="${details_pr_release[0]}"
deleted_lines_pr_release="${details_pr_release[1]}"
if [ "$added_lines_pr_development" != "$added_lines_pr_release" ] || [ "$deleted_lines_pr_development" != "$deleted_lines_pr_release" ]; then
if [ "$added_lines_pr_master" != "$added_lines_pr_release" ] || [ "$deleted_lines_pr_master" != "$deleted_lines_pr_release" ]; then
echo "File $file has different added or deleted lines in the two pull requests."
exit 1
fi

5
.gitignore vendored
View File

@@ -23,3 +23,8 @@ local.env
# Local build cache
build-cache
api-credentials.json
node_modules/*/android/build/*
node_modules
config.js
android/navi-base/.cxx/*
android/npci-upi-cl/build/*

57
App.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { Component } from "react";
import codePush from "react-native-code-push";
import { CtaData } from "./App/common/interface";
import RnApp from "./App/common/navigator/RnAppCreator";
import { getBuildConfigDetails, setBuildConfigDetails } from "./App/common/utilities/CacheUtils";
import { logToSentry } from "./App/common/hooks/useSentryLogging";
export default class App extends Component<{}> {
checkForUpdates = () => {
let flavor: string | undefined
getBuildConfigDetails().then((res) => {
flavor = res?.baseUrl
})
codePush.sync({
updateDialog: flavor && flavor === "QA" ? {appendReleaseDescription: true} : {appendReleaseDescription: false},
installMode: codePush.InstallMode.IMMEDIATE,
mandatoryInstallMode: codePush.InstallMode.IMMEDIATE,
});
};
getInitialCta = (): CtaData | undefined => {
const { CtaData } = this.props as any;
if (!CtaData) {
logToSentry(
`CtaData is missing or invalid: ${CtaData} | MethodName: getInitialCta`
);
return;
}
try {
const cta = JSON.parse(CtaData) as CtaData;
return cta;
} catch (error) {
logToSentry(
`Error parsing CtaData: ${CtaData} | Error: ${error} | MethodName: getInitialCta`
);
return;
}
};
override componentDidMount(): void {
setBuildConfigDetails()
this.checkForUpdates();
}
override render() {
const cta = this.getInitialCta();
if (!!cta) {
return RnApp.create(cta);
} else {
// return error screen
}
}
}
//return RnNavigator.navigate(screenBundle);

View File

@@ -0,0 +1,28 @@
import { StyleSheet } from "react-native";
export const commonStyles = StyleSheet.create({
container: {
backgroundColor: "red",
},
fullscreencenter: {
justifyContent: "center",
alignItems: "center",
flex: 1,
},
flex_1: {
flex: 1,
},
verticalSpacer32: {
height: 32,
},
selfAlignCenter: {
alignSelf: "center",
},
contentAlignLeft: {
flexDirection: "column",
alignItems: "flex-start",
},
height54: {
height: 54,
},
});

View File

@@ -0,0 +1,9 @@
import IntroScreen from "./screen/IntroScreen";
import InsuranceLandingPageScreen from "./screen/InsuranceLandingPageScreen";
import QuoteOfferScreen from "./screen/quote-offer-screen/QuoteOfferScreen";
export {
IntroScreen,
InsuranceLandingPageScreen,
QuoteOfferScreen,
};

View File

@@ -0,0 +1,13 @@
import { getXTargetHeaderInfo } from "../../../../network/ApiClient";
import { get, post, patch } from "../../../../network/NetworkService";
import { GI } from "../../../common/constants/NavigationHandlerConstants";
import { ScreenData } from "../../../common/interface/widgets/screenData/ScreenData";
export interface SumInsuredRequestData {
sumInsured: string;
}
export const updateSumInsuredData = async (data: SumInsuredRequestData, quoteId: string) => {
const url = `v3/quotes/${quoteId}`;
return patch<ApiResponse<ScreenData>>(url, data, getXTargetHeaderInfo(GI.toLocaleUpperCase()));
};

View File

@@ -0,0 +1,147 @@
import React, { useEffect, useState } from "react";
import {
Animated,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
View,
StyleSheet
} from "react-native";
import {
BaseActionTypes,
GenericActionPayload,
} from "../../../common/actions/GenericAction";
import { CtaData, CtaType } from "../../../common/interface";
import { Widget } from "../../../common/interface/widgets/Widget";
import { ScreenData } from "../../../common/interface/widgets/screenData/ScreenData";
import { NativeDeeplinkNavigatorModule } from "../../../common/native-module/NativeModules";
import Colors from "../../../../assets/colors/colors";
import BaseWidget from "../../../../components/widgets/BaseWidget";
import { ScreenActionTypes } from "../../../common/screen/ScreenActionTypes";
const InsuranceLandingPageScreen = ({
ctaData,
screenData,
handleActions,
}: {
ctaData: CtaData;
screenData: ScreenData | null;
handleActions: (screenPayload?: GenericActionPayload) => void;
}) => {
const [scrollY, setScrollY] = useState(0);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
setScrollY(event.nativeEvent.contentOffset.y);
};
const headerBackgroundColor =
scrollY > 84 ? Colors.aliceBlue : Colors.transparent;
const handleClick = (cta?: CtaData) => {
if (!cta) return; // Handle case when cta is undefined or null
try {
switch (cta.type) {
case CtaType.DEEP_LINK:
case CtaType.USE_ROOT_DEEPLINK_NAVIGATOR:
NativeDeeplinkNavigatorModule.navigateToNaviInsuranceDeeplinkNavigator(
JSON.stringify(cta)
);
break;
default:
NativeDeeplinkNavigatorModule.navigateToNaviDeeplinkNavigator(
JSON.stringify(cta)
);
break;
}
} catch (error) {
// #TODO: Handle the error gracefully using Sentry.
console.error("Error while navigating to deep link:", error);
}
};
useEffect(() => {
handleActions({
baseActionType: BaseActionTypes.SCREEN_ACTION,
metaData: [
{
actionType: ScreenActionTypes.FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND,
},
],
});
}, []);
return (
<View style={[styles.container, screenData?.screenStyle]}>
<Animated.View
style={[styles.header, { backgroundColor: headerBackgroundColor }]}
>
{getWidgetViews(
screenData?.screenWidgets?.headerWidgets,
handleActions,
handleClick
)}
</Animated.View>
<ScrollView
contentContainerStyle={styles.content}
scrollEventThrottle={16}
onScroll={handleScroll}
>
{getWidgetViews(
screenData?.screenWidgets?.contentWidgets,
handleActions,
handleClick
)}
</ScrollView>
<View style={styles.footer}>
{getWidgetViews(
screenData?.screenWidgets?.footerWidgets,
handleActions,
handleClick
)}
</View>
</View>
);
};
function getWidgetViews(
widgetList: Widget[] | undefined,
handleActions: (screenActionPayload?: GenericActionPayload) => void,
handleClick?: (ctaData: CtaData) => void
): React.JSX.Element {
return (
<View>
{widgetList?.map((widget, index) => {
return (
<BaseWidget
widget={widget}
handleScreenActions={handleActions}
widgetIndex={index}
key={index}
handleClick={handleClick}
/>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "column",
},
header: {
alignItems: "stretch",
position: "absolute",
zIndex: 1,
},
content: {
flexGrow: 1
},
footer: {
alignItems: "stretch"
},
});
export default InsuranceLandingPageScreen;

View File

@@ -0,0 +1,22 @@
import React, { useEffect } from "react";
import { Text, View } from "react-native";
import { getApplicationId } from "../../../../network/repo/GiApplicationRepo";
import { CtaData } from "../../../common/interface";
const IntroScreen: React.FC<{ ctaData: CtaData }> = ({ ctaData }) => {
useEffect(() => {
getApplicationId().then(
(result) =>
// console.log("klogs",result.data)
result.data
);
}, []);
return (
<View>
<Text>hello</Text>
</View>
);
};
export default IntroScreen;

View File

@@ -0,0 +1,274 @@
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
Dimensions,
ScrollView,
Pressable,
ToastAndroid,
} from "react-native";
import { CtaData } from "../../../common/interface";
import {
fillApplication,
getApplicationId,
} from "../../../../network/repo/GiApplicationRepo";
import { GetWidgetView } from "../../../common/widgets/widgetResolver";
import { CtaNavigator } from "../../../common/navigator/NavigationRouter";
import { MemberDetailsRootObject } from "../../../common/interface/MemberDetailsResponse";
const { width } = Dimensions.get("window");
const MemberDetailScreen: React.FC<{ ctaData: CtaData }> = ({ ctaData }) => {
const [screenData, setScreenData] = useState<
MemberDetailsRootObject | undefined
>();
const [dataFromChild, setDataFromChild] = useState([]);
const [isDataValid, setIsDataValid] = useState(true);
useEffect(() => {
getApplicationId().then((result) => {
setScreenData(result.data);
});
}, []);
const sendDataToBackend = () => {
const widgetList =
screenData?.currentScreenDefinition?.screenData?.screenStructure?.content
?.widgets;
if (
isDataValid === true &&
widgetList &&
dataFromChild.length == (widgetList.length - 1) * 2
) {
fillApplication(
screenData?.applicationResponse?.applicationId || "",
dataFromChild
).then((fillApplicationResponse) => {
const cta: CtaData = {
url: "react/member_selection",
};
CtaNavigator.navigate(cta);
});
} else {
ToastAndroid.show("Please fill all data", ToastAndroid.SHORT);
}
};
const checkValidity = (updatedData: any[]) => {
let isValid = true;
updatedData.map((item) => {
if (
item.value === undefined ||
item.value === null ||
item.value === ""
) {
isValid = false;
}
});
setIsDataValid(isValid);
};
// Callback function to receive data from the child component
const handleDataFromChild = (index: number, data: any) => {
let updatedData: any[] = [...dataFromChild];
let isPresent = false;
updatedData.map((oldData) => {
// if the key is present replace existing.
if (oldData.key === data.key) {
isPresent = true;
oldData.value = data.value;
if (
oldData.value === undefined ||
oldData.value === null ||
oldData.value === ""
) {
setIsDataValid(false);
} else {
checkValidity(updatedData);
}
}
});
if (!isPresent) {
updatedData = [...updatedData, data];
}
console.log(updatedData);
// updatedData[index] = data;
setDataFromChild(updatedData);
console.log();
};
useEffect(() => {
console.log("HLOGS", screenData);
}, [screenData]);
const getScreenContent = () => {
return (
<View style={styles.container}>
<InsuranceHeaderSection
cardList={null}
setCardList={null}
itemRemovedCount={null}
setItemRemovedCount={null}
db={null}
/>
{/* Content */}
<ScrollView
style={styles.content}
contentContainerStyle={{
flexGrow: 1,
}}
keyboardShouldPersistTaps="handled"
>
{screenData?.currentScreenDefinition?.screenData?.screenStructure?.content?.widgets.map(
(widget: Widget, index: number) => {
//setDataFromChild((prevData) => [...prevData, {}])
return GetWidgetView.getWidget(
widget,
handleDataFromChild,
index - 1
);
}
)}
</ScrollView>
{/* Footer */}
{/* Green Tag */}
<View style={styles.greenTag}>
<Text style={styles.greenTagText}>
{
screenData?.currentScreenDefinition?.screenData?.screenStructure
?.footer?.widgetData?.banner?.text
}
</Text>
</View>
<View style={styles.footer}>
<View style={styles.infoContainer}>
<Text style={styles.infoTopText}>
{
screenData?.currentScreenDefinition?.screenData?.screenStructure
?.footer?.widgetData?.leftTopUnstrikedText?.text
}{" "}
<Text style={styles.strikeOffText}>
{
screenData?.currentScreenDefinition?.screenData
?.screenStructure?.footer?.widgetData?.leftTopStrikedText
?.text
}
</Text>
<Text style={styles.strikeOffSubText}></Text>
</Text>
<Text style={styles.infoBottomText}>
{
screenData?.currentScreenDefinition?.screenData?.screenStructure
?.footer?.widgetData?.leftBottomText?.text
}{" "}
&gt;
</Text>
</View>
<Pressable
android_ripple={{ color: "gray", borderless: false }}
style={styles.buttonContainer}
onPress={() => sendDataToBackend()}
>
<Text style={styles.buttonText}>
{
screenData?.currentScreenDefinition?.screenData?.screenStructure
?.footer?.widgetData?.rightCta?.title?.text
}
</Text>
</Pressable>
</View>
</View>
);
};
return <View style={styles.container}>{getScreenContent()}</View>;
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#FAFAFA",
},
content: {
flex: 1,
paddingVertical: 20,
},
footer: {
elevation: 10,
backgroundColor: "#FFFFFF",
paddingHorizontal: 16,
paddingBottom: 32,
paddingTop: 20,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
infoContainer: {
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
},
infoTopText: {
fontSize: 18,
fontWeight: "500",
color: "#191919",
alignSelf: "flex-start",
},
strikeOffText: {
fontSize: 14,
fontWeight: "500",
color: "#6B6B6B",
alignSelf: "flex-start",
textDecorationLine: "line-through",
},
strikeOffSubText: {
fontSize: 14,
fontWeight: "500",
color: "#6B6B6B",
alignSelf: "flex-start",
textDecorationLine: "line-through",
},
infoBottomText: {
fontSize: 12,
fontWeight: "bold",
color: "#1f002a",
marginTop: 4,
},
buttonContainer: {
backgroundColor: "#1F002A",
height: 48,
width: 148,
borderRadius: 4,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
color: "white",
fontWeight: "bold",
},
greenTag: {
elevation: 10,
backgroundColor: "#E7F8EE",
width: width,
height: 32,
justifyContent: "center",
paddingLeft: 16,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
greenTagText: {
fontSize: 12,
color: "#22A940",
textAlign: "center",
fontWeight: "bold",
},
});
export default MemberDetailScreen;

View File

@@ -0,0 +1,78 @@
import { TouchableOpacity, View, Text } from "react-native";
import { GenericActionPayload } from "../../../../common/actions/GenericAction";
import { CtaData, CtaType } from "../../../../common/interface";
import { ScreenData } from "../../../../common/interface/widgets/screenData/ScreenData";
import { AppImage } from "../../../../../components/AppImage";
import {
ImageName,
QUOTE_APOLOGY_BUTTON,
QUOTE_APOLOGY_SUBTITLE,
QUOTE_APOLOGY_TITLE,
} from "../../../../common/constants/StringConstant";
import { NativeDeeplinkNavigatorModule } from "../../../../common/native-module/NativeModules";
import { styles } from "./QuoteApologyScreenStyle";
import { StaticHeader } from "../../../../../components/reusable/static-header/StaticHeader";
import { ConstantCta } from "../../../../common/constants/CtaConstants";
const QuoteApologyScreen = ({
ctaData,
screenData,
handleActions,
}: {
ctaData: CtaData;
screenData: ScreenData | null;
handleActions: (screenPayload?: GenericActionPayload) => void;
}) => {
const handleClick = (cta?: CtaData) => {
if (!cta) return; // Handle case when cta is undefined or null
try {
switch (cta.type) {
case CtaType.DEEP_LINK:
case CtaType.USE_ROOT_DEEPLINK_NAVIGATOR:
NativeDeeplinkNavigatorModule.navigateToNaviInsuranceDeeplinkNavigator(
JSON.stringify(cta)
);
break;
default:
NativeDeeplinkNavigatorModule.navigateToNaviDeeplinkNavigator(
JSON.stringify(cta)
);
break;
}
} catch (error) {
// #TODO: Handle the error gracefully using Sentry.
console.error("Error while navigating to deep link:", error);
}
};
const onPress = () => {
handleClick && handleClick(ConstantCta.QUOTE_APOLOGY_FOOTER_BUTTON);
};
return (
<View style={styles.container}>
<View>
<StaticHeader
handleClick={handleClick}
leftIconCta={ConstantCta.STATIC_HEADER_LEFT_ICON_CTA}
rightIconCta={ConstantCta.STATIC_HEADER_RIGHT_ICON_CTA}
/>
<View style={styles.headerBorder} />
</View>
<View style={styles.centerContent}>
{AppImage(ImageName.QUOTE_APOLOGY_ICON, styles.centerIcon)}
<Text style={styles.title}>{QUOTE_APOLOGY_TITLE}</Text>
<Text style={styles.subtitle}>{QUOTE_APOLOGY_SUBTITLE}</Text>
</View>
<TouchableOpacity
onPress={onPress}
style={styles.footerButton}
activeOpacity={1}
>
<Text style={styles.buttonText}>{QUOTE_APOLOGY_BUTTON}</Text>
</TouchableOpacity>
</View>
);
};
export default QuoteApologyScreen;

View File

@@ -0,0 +1,55 @@
import { StyleSheet } from "react-native";
export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "space-between",
backgroundColor: "white",
},
centerContent: {
justifyContent: "center",
alignItems: "center"
},
headerBorder: {
width: "100%",
height: 1,
backgroundColor: "#F0F0F0"
},
centerIcon: {
width: 225,
height: 225
},
title: {
fontSize: 16,
lineHeight: 22,
fontFamily: "tt_medium",
color: "#191919",
textAlign: "center",
marginTop: 16,
marginHorizontal: 20
},
subtitle: {
fontSize: 14,
lineHeight: 22,
fontFamily: "tt_regular",
color: "#4D4D4D",
textAlign: "center",
marginTop: 16,
marginHorizontal: 20
},
footerButton: {
padding: 16,
marginHorizontal: 16,
marginVertical: 16,
backgroundColor: "#1F002A",
borderRadius: 4,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
color: "white",
fontSize: 14,
lineHeight: 22,
fontFamily: "tt_medium",
},
});

View File

@@ -0,0 +1,268 @@
import React, { useEffect } from "react";
import {
NativeEventEmitter,
NativeScrollEvent,
NativeSyntheticEvent,
StatusBar,
View,
} from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import Colors from "../../../../../assets/colors/colors";
import BaseWidget from "../../../../../components/widgets/BaseWidget";
import {
BaseActionTypes,
GenericActionPayload,
} from "../../../../common/actions/GenericAction";
import {
HI_RN_QUOTE_PAGE_ERROR_VIEW,
HI_RN_QUOTE_PAGE_INIT,
} from "../../../../common/constants/AnalyticsEventsConstant";
import { NativeEventNameConstants } from "../../../../common/constants/EventNameConstants";
import {
HEADER_LOTTIE_TITLE_HEIGHT,
HEADER_LOTTIE_WIDGET_HEIGHT,
} from "../../../../common/constants/NumericalConstants";
import { sendAsAnalyticsEvent } from "../../../../common/hooks/useAnalyticsEvent";
import { AnalyticsEvent, CtaData, CtaType } from "../../../../common/interface";
import { Widget } from "../../../../common/interface/widgets/Widget";
import { ScreenData } from "../../../../common/interface/widgets/screenData/ScreenData";
import { NativeDeeplinkNavigatorModule } from "../../../../common/native-module/NativeModules";
import { ScreenState } from "../../../../common/screen/BaseScreen";
import { ScreenActionTypes } from "../../../../common/screen/ScreenActionTypes";
import styles from "./QuoteOfferScreenStyle";
import QuoteOfferErrorScreen from "./error-screen/QuoteOfferErrorScreen";
import QuoteOfferShimmerScreen from "./shimmer-screen/QuoteOfferShimmerScreen";
import { logToSentry } from "../../../../common/hooks/useSentryLogging";
const QuoteOfferScreen = ({
ctaData,
screenData,
handleActions,
}: {
ctaData: CtaData;
screenData: ScreenData | null;
handleActions: (screenPayload?: GenericActionPayload) => void;
}) => {
const y = useSharedValue(0);
// TODO: check and remove below code if it is working fine in release.
// const onScroll = useAnimatedScrollHandler({
// onScroll: (event) => {
// y.value = event.contentOffset.y;
// },
// });
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
y.value = event.nativeEvent.contentOffset.y;
};
const headerBgStyle = useAnimatedStyle(() => {
return {
backgroundColor:
y.value > HEADER_LOTTIE_WIDGET_HEIGHT
? Colors.grey
: y.value > HEADER_LOTTIE_TITLE_HEIGHT
? Colors.aliceBlue
: Colors.transparent,
};
});
const screenOverlayStyle =
screenData?.screenState === ScreenState.OVERLAY
? { opacity: 1 }
: { opacity: 1 };
let preQuoteId: string | undefined | null = undefined;
let quoteId: string | undefined | null = undefined;
ctaData?.parameters?.forEach((item) => {
if (item.key === "preQuoteId") {
preQuoteId = item.value;
}
if (item.key === "quoteId") {
quoteId = item.value;
}
});
const handleClick = (cta?: CtaData) => {
if (!cta) {
logToSentry(
`Navigation cta is missing or invalid: ${cta} | MethodName: handleClick}`
);
return;
}
try {
switch (cta.type) {
case CtaType.DEEP_LINK:
case CtaType.USE_ROOT_DEEPLINK_NAVIGATOR:
NativeDeeplinkNavigatorModule.navigateToNaviInsuranceDeeplinkNavigator(
JSON.stringify(cta)
);
break;
default:
NativeDeeplinkNavigatorModule.navigateToNaviDeeplinkNavigator(
JSON.stringify(cta)
);
break;
}
} catch (error) {
logToSentry(
`Error while navigating to deep link with CTA: ${cta} | MethodName: handleClick}`
);
}
};
useEffect(() => {
const screenActionType = preQuoteId
? ScreenActionTypes.FETCH_QUOTE_V3
: ScreenActionTypes.FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND;
const data: QuoteOfferRequest = {
preQuoteId: preQuoteId ? preQuoteId : undefined,
quoteId: quoteId ? quoteId : undefined,
};
const nativeEventListener = new NativeEventEmitter();
let reloadPageEventListener = nativeEventListener.addListener(
NativeEventNameConstants.reloadPage,
(event) => {
if (event === true) {
handleActions({
baseActionType: BaseActionTypes.SCREEN_ACTION,
metaData: [
{
actionType: screenActionType,
data: data,
},
],
});
}
}
);
handleActions({
baseActionType: BaseActionTypes.SCREEN_ACTION,
metaData: [
{
actionType: screenActionType,
data: data,
},
],
});
return () => {
nativeEventListener.removeAllListeners(
NativeEventNameConstants.reloadPage
);
};
}, [ctaData, preQuoteId, quoteId]);
useEffect(() => {
switch (screenData?.screenState) {
case ScreenState.LOADED:
const initEvent: AnalyticsEvent = {
name: HI_RN_QUOTE_PAGE_INIT,
};
sendAsAnalyticsEvent(initEvent);
break;
case ScreenState.ERROR:
const errorEvent: AnalyticsEvent = {
name: HI_RN_QUOTE_PAGE_ERROR_VIEW,
};
sendAsAnalyticsEvent(errorEvent);
break;
default:
break;
}
}, [screenData?.screenState]);
useEffect(() => {
if (
screenData?.screenState !== ScreenState.ERROR &&
screenData?.screenState !== ScreenState.LOADING
) {
handleClick(screenData?.screenMetaData?.redirectionCta);
}
}, [screenData?.screenMetaData?.redirectionCta]);
if (screenData?.screenState === ScreenState.LOADING) {
return <QuoteOfferShimmerScreen handleClick={handleClick} />;
} else if (screenData?.screenState === ScreenState.ERROR) {
return (
<QuoteOfferErrorScreen
errorMetaData={screenData.errorMetaData}
handleActions={handleActions}
handleClick={handleClick}
/>
);
} else {
return (
<View style={[styles.container, screenData?.screenStyle]}>
<StatusBar backgroundColor={Colors.white} barStyle="dark-content" />
<Animated.View style={[styles.header, headerBgStyle]}>
{getWidgetViews(
screenData?.screenWidgets?.headerWidgets,
handleActions,
screenData?.screenState,
handleClick
)}
</Animated.View>
<Animated.ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.content}
scrollEventThrottle={16}
onScroll={onScroll}
nestedScrollEnabled={true}
style={screenOverlayStyle}
pointerEvents={
screenData?.screenState === ScreenState.OVERLAY ? "none" : "auto"
}
>
{getWidgetViews(
screenData?.screenWidgets?.contentWidgets,
handleActions,
screenData?.screenState,
handleClick
)}
</Animated.ScrollView>
<View
style={[styles.footer, screenOverlayStyle]}
pointerEvents={
screenData?.screenState === ScreenState.OVERLAY ? "none" : "auto"
}
>
{getWidgetViews(
screenData?.screenWidgets?.footerWidgets,
handleActions,
screenData?.screenState,
handleClick
)}
</View>
</View>
);
}
};
function getWidgetViews(
widgetList: Widget[] | undefined,
handleActions: (screenActionPayload?: GenericActionPayload) => void,
screenState?: ScreenState | null,
handleClick?: (ctaData: CtaData) => void
): React.JSX.Element {
return (
<View>
{widgetList?.map((widget, index) => {
return (
<BaseWidget
widget={widget}
handleScreenActions={handleActions}
screenState={screenState}
widgetIndex={index}
key={index}
handleClick={handleClick}
/>
);
})}
</View>
);
}
export default QuoteOfferScreen;

View File

@@ -0,0 +1,21 @@
import { StyleSheet } from "react-native";
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "column",
},
header: {
alignItems: "stretch",
position: "absolute",
zIndex: 1,
},
content: {
flexGrow: 1,
},
footer: {
alignItems: "stretch",
},
});
export default styles;

View File

@@ -0,0 +1,52 @@
import { View, Text } from "react-native";
import { GenericActionPayload } from "../../../../../common/actions/GenericAction";
import { CtaData } from "../../../../../common/interface";
import { StaticHeader } from "../../../../../../components/reusable/static-header/StaticHeader";
import { styles } from "./QuoteOfferErrorScreenStyle";
import { TouchableOpacity } from "react-native-gesture-handler";
import {
ERROR_SUBTITLE,
ERROR_TITLE,
RETRY,
ImageName,
} from "../../../../../common/constants/StringConstant";
import { AppImage } from "../../../../../../components/AppImage";
import { ConstantCta } from "../../../../../common/constants/CtaConstants";
const QuoteOfferErrorScreen = ({
errorMetaData,
handleActions,
handleClick,
}: {
errorMetaData?: GenericActionPayload;
handleActions?: (screenPayload?: GenericActionPayload) => void;
handleClick?: (cta: CtaData) => void;
}) => {
const onPress = () => {
handleActions && handleActions(errorMetaData);
};
return (
<View style={styles.container}>
<StaticHeader
handleClick={handleClick}
leftIconCta={ConstantCta.STATIC_HEADER_LEFT_ICON_CTA}
rightIconCta={ConstantCta.STATIC_HEADER_RIGHT_ICON_CTA}
/>
<View style={styles.centerContent}>
{AppImage(ImageName.SWW, styles.centerIcon)}
<Text style={styles.errorTitle}>{ERROR_TITLE}</Text>
<Text style={styles.errorSubtitle}>{ERROR_SUBTITLE}</Text>
</View>
<TouchableOpacity
onPress={onPress}
style={styles.retryButton}
activeOpacity={1}
>
<Text style={styles.buttonText}>{RETRY}</Text>
</TouchableOpacity>
</View>
);
};
export default QuoteOfferErrorScreen;

View File

@@ -0,0 +1,48 @@
import { StyleSheet } from "react-native";
export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "space-between",
backgroundColor: "white",
},
centerContent: {
justifyContent: "center",
alignItems: "center",
},
centerIcon: {
width: 125,
height: 125,
},
errorTitle: {
fontSize: 16,
lineHeight: 22,
fontFamily: "tt_semi_bold",
color: "#191919",
textAlign: "center",
marginTop: 32,
},
errorSubtitle: {
fontSize: 14,
lineHeight: 22,
fontFamily: "tt_regular",
color: "#6B6B6B",
textAlign: "center",
marginTop: 16,
},
retryButton: {
padding: 16,
marginHorizontal: 16,
marginVertical: 32,
backgroundColor: "#1F002A",
borderRadius: 4,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
color: "white",
fontSize: 14,
lineHeight: 22,
fontFamily: "tt_semi_bold",
},
});

View File

@@ -0,0 +1,87 @@
import { View } from "react-native";
import SkeletonPlaceholder from "react-native-skeleton-placeholder";
import Colors from "../../../../../../assets/colors/colors";
import styles from "./QuoteOfferShimmerScreenStyle";
import { StaticHeader } from "../../../../../../components/reusable/static-header/StaticHeader";
import { CtaData } from "../../../../../common/interface";
import { ConstantCta } from "../../../../../common/constants/CtaConstants";
const QuoteOfferShimmerScreen = ({
handleClick,
}: {
handleClick?: (ctaData: CtaData) => void;
}) => {
return (
<View style={styles.container}>
<View style={styles.header}>
<StaticHeader
handleClick={handleClick}
leftIconCta={ConstantCta.STATIC_HEADER_LEFT_ICON_CTA}
rightIconCta={ConstantCta.STATIC_HEADER_RIGHT_ICON_CTA}
/>
</View>
<ContentShimmer />
<FooterShimmer />
</View>
);
};
export default QuoteOfferShimmerScreen;
const HeaderShimmer = () => {
return (
<View style={styles.header}>
<SkeletonPlaceholder
backgroundColor={Colors.shimmerBgColor}
highlightColor={Colors.shimmerHighlightColor}
direction="right"
enabled={true}
angle={100}
borderRadius={4}
>
<View>
<View style={styles.shimmerHeaderLayout} />
</View>
</SkeletonPlaceholder>
</View>
);
};
const ContentShimmer = () => {
return (
<View style={styles.content}>
<SkeletonPlaceholder
backgroundColor={Colors.shimmerBgColor}
highlightColor={Colors.shimmerHighlightColor}
direction="right"
angle={100}
borderRadius={4}
>
<View>
<View style={styles.shimmerLayout1} />
<View style={styles.shimmerLayout2} />
<View style={styles.shimmerLayout3} />
<View style={styles.shimmerLayout4} />
</View>
</SkeletonPlaceholder>
</View>
);
};
const FooterShimmer = () => {
return (
<View style={styles.footer}>
<SkeletonPlaceholder
backgroundColor={Colors.shimmerBgColor}
highlightColor={Colors.shimmerHighlightColor}
direction="right"
angle={100}
borderRadius={4}
>
<View>
<View style={styles.shimmerFooterLayout} />
</View>
</SkeletonPlaceholder>
</View>
);
};

View File

@@ -0,0 +1,108 @@
import { StyleSheet } from "react-native";
const styles = StyleSheet.create({
shimmerHeader: {
alignItems: "stretch",
position: "absolute",
zIndex: 1,
},
shimmerContent: {},
shimmerFooterContainer: {
height: 100,
zIndex: 2,
position: "absolute",
bottom: 0,
},
container: {
flex: 1,
flexDirection: "column",
justifyContent: "center",
},
header: {
position: "absolute",
top: 0,
width: "100%",
backgroundColor: "white",
},
content: {
marginTop: 88,
flexGrow: 1,
marginHorizontal: 16,
},
footer: {
borderTopStartRadius: 16,
borderTopEndRadius: 16,
shadowColor: "black",
shadowOpacity: 0.26,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 10,
elevation: 3,
backgroundColor: "white",
},
shimmerLayout1: {
position: "relative",
marginTop: 12,
height: 152,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
shimmerLayout2: {
position: "relative",
marginTop: 24,
height: 78,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
shimmerLayout3: {
position: "relative",
marginTop: 24,
height: 148,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
shimmerLayout4: {
position: "relative",
marginTop: 24,
height: 35,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
shimmerFooterLayout: {
position: "relative",
marginHorizontal: 16,
marginTop: 16,
marginBottom: 24,
height: 49,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
shimmerHeaderLayout: {
position: "relative",
marginHorizontal: 16,
marginTop: 16,
marginBottom: 24,
height: 49,
alignItems: "center",
borderRadius: 4,
flexShrink: 0,
gap: 10.5,
},
});
export default styles;

View File

@@ -0,0 +1,37 @@
import { Dispatch, SetStateAction } from "react";
import { AnalyticsEvent, CtaData } from "../interface";
import { ScreenData } from "../interface/widgets/screenData/ScreenData";
import { ScreenState } from "../screen/BaseScreen";
export interface GenericActionPayload {
baseActionType?: string;
type?: string; // type is used for analytics seggregations and also screen level API call or handling seggregation
metaData?: ActionMetaData[];
ctaData?: CtaData;
setScreenData?: Dispatch<SetStateAction<ScreenData | null>>;
setScreenState?: Dispatch<SetStateAction<ScreenState | null>>;
setErrorMetaData?: Dispatch<SetStateAction<ActionMetaData[] | null>>;
screenData?: ScreenData | null;
}
export interface ActionMetaData {
actionType?: string;
// ActionMetaData has some key attributes required to perform any action e.g. critical widget communications i.e. inter/intra widget, screen level API calls, bottom sheet, modal, etc.
data?: any;
analyticsEventProperties?: AnalyticsEvent;
}
export interface TargetWidgetPayload {
targetWidgetId?: string;
keyPath?: string;
newValue?: any;
valueType: string;
widgetId?: string;
actionId?: string;
}
export const BaseActionTypes = {
SCREEN_ACTION: "SCREEN_ACTION",
WIDGET_ACTION: "WIDGET_ACTION",
ANALYTICS_EVENT_ACTION: "CLICKSTREAM_ACTION",
};

View File

@@ -0,0 +1,4 @@
export const HI_SI_PILLS_CLICK = "hi_si_pills_click"
export const SUM_INSURED = "sum_insured";
export const HI_RN_QUOTE_PAGE_INIT = "hi_rn_quote_page_init";
export const HI_RN_QUOTE_PAGE_ERROR_VIEW = "hi_rn_quote_page_error_view";

View File

@@ -0,0 +1,25 @@
export const ConstantCta = {
STATIC_HEADER_LEFT_ICON_CTA: {
url: "home",
},
STATIC_HEADER_RIGHT_ICON_CTA: {
url: "PRODUCT_HELP_PAGE",
type: "USE_ROOT_DEEPLINK_NAVIGATOR",
finish: false,
parameters: [
{
key: "SCREEN_NAME",
value: "GI_QUOTE_FORM_V2_SCREEN",
},
],
},
QUOTE_APOLOGY_FOOTER_BUTTON: {
url: "gi/insurance_container/pre_quote_journey",
parameters: [
{
key: "applicationType",
value: "FRESH_POLICY",
},
],
},
};

View File

@@ -0,0 +1,3 @@
export enum NativeEventNameConstants {
reloadPage = "reloadPage",
}

View File

@@ -0,0 +1,3 @@
export const PREMIUM_DETAILS_BOTTOM_SHEET = "premium_details_bottom_sheet";
export const TITLE_WITH_FEEDBACK_PILL_BOTTOM_SHEET = "title_with_feedback_pill_bottom_sheet";
export const TITLE_WITH_STEPS_BOTTOM_SHEET = "title_with_steps_bottom_sheet"

View File

@@ -0,0 +1 @@
export const GI = "gi";

View File

@@ -0,0 +1,2 @@
export const HEADER_LOTTIE_WIDGET_HEIGHT = 180;
export const HEADER_LOTTIE_TITLE_HEIGHT = 84;

View File

@@ -0,0 +1,9 @@
export const INTRO_SCREEN = "intro";
export const SECOND_INTRO_SCREEN = "second_intro";
export const MEMBER_SELECTION_SCREEN = "member_selection";
export const HOME_SCREEN = "home";
export const BASE_SCREEN = "base";
export const INSURANCE_LANDING_PAGE_SCREEN = "insurance_landing_page";
export const QUOTE_OFFER_SCREEN = "quote_offer";
export const BUY_INSURANCE_SCREEN = "buyinsurance";
export const QUOTE_APOLOGY_SCREEN = "fresh_policy_form";

View File

@@ -0,0 +1,29 @@
export const Orientation = {
VERTICAL: "vertical",
HORIZONTAL: "horizontal",
DIAGONAL: "diagonal",
};
export const Lottie = {
FOOTER_LOADER_URL:
"https://public-assets.prod.navi-sa.in/home_uitron/cta_loader.json",
};
export const ImageName = {
SWW: "SWW",
CROSS: "CROSS",
HELP: "HELP",
QUOTE_APOLOGY_ICON: "QUOTE_APOLOGY_ICON",
};
export const ERROR_TITLE = "Something went wrong";
export const ERROR_SUBTITLE = "Please try again after some time";
export const RETRY = "Retry";
export const QUOTE_APOLOGY_TITLE =
"Sorry, we cannot insure some members with health issues!";
export const QUOTE_APOLOGY_SUBTITLE =
"You can still purchase policy for others members";
export const QUOTE_APOLOGY_BUTTON = "Buy new policy";
export const QUOTE_PATCH_FAIL_TOAST = "Failed. Try again";
export const QUOTE_ID = "quoteId";
export const BUILD_CONFIG_DETAILS = "BUILD_CONFIG_DETAILS";

View File

@@ -0,0 +1,15 @@
export const INFO_DISPLAY_WIDGET = "RN_DEMO_INFO_ICON_TITLE_CARD";
export const NAME_DOB_INPUT_WIDGET = "RN_DEMO_LABEL_INPUT_WITH_DOB_PICKER";
export const TITLE_WIDGET = "TITLE_WIDGET";
export const BUTTON_TEST_WIDGET = "BUTTON_TEST_WIDGET";
export const SLIDER_WIDGET = "SLIDER_WIDGET";
export const TITLE_WITH_LIST_WIDGET = "TITLE_WITH_LIST_WIDGET";
export const FOOTER_WITH_CARD_WIDGET = "FOOTER_WITH_CARD_WIDGET";
export const GRID_WITH_CARD_WIDGET = "GRID_WITH_CARD_WIDGET";
export const COMPARISON_WIDGET = "COMPARISON_WIDGET";
export const TITLE_WITH_ASSETS_WIDGET = "TITLE_WITH_ASSETS_WIDGET";
export const HEADER_WITH_ASSETS_WIDGET = "HEADER_WITH_ASSETS_WIDGET";
export const HEADER_LOTTIE_ANIMATION_WIDGET = "HEADER_LOTTIE_ANIMATION_WIDGET";
export const SUM_INSURED_WIDGET = "SUM_INSURED_WIDGET";
export const TITLE_SUBTITLE_WITH_ASSET_WIDGET = "TITLE_SUBTITLE_WITH_ASSET_WIDGET";
export const FAB_REQUEST_TO_CALLBACK = "FAB_REQUEST_TO_CALLBACK";

View File

@@ -0,0 +1,70 @@
import { AnalyticsEvent } from "../interface";
import { NativeAnalyticsModule } from "../native-module/NativeModules";
import { useAsRecord } from "../utilities/RecordUtils";
export const sendAsAnalyticsEvent = (analyticsEvent: AnalyticsEvent) => {
const eventName = analyticsEvent.name;
const eventProperties = analyticsEvent.properties;
NativeAnalyticsModule.sendAsAnalyticsEvent(
eventName,
useAsRecord(eventProperties)
);
};
export const sendAsAppDowntimeEvent = (event: AppDowntimeData) => {
const {
reason = null,
screenName = null,
moduleName = null,
statusCode = null,
networkType = null,
flowName = null,
methodName = null,
vendorName = null,
extras = null,
eventName = "global_app_downtime",
} = event;
NativeAnalyticsModule.sendAsAppDowntimeEvent({
reason,
screenName,
moduleName,
statusCode,
networkType,
flowName,
methodName,
vendorName,
extras,
eventName,
});
};
export const sendAsGlobalErrorEvent = (event: GlobalErrorData) => {
const {
reason = null,
source = null,
moduleName,
globalErrorType,
statusCode = null,
networkType = null,
flowName = null,
methodName = null,
vendorName = null,
extras = null,
journeySource = null,
} = event;
NativeAnalyticsModule.sendAsGlobalErrorEvent({
reason,
source,
moduleName,
globalErrorType,
statusCode,
networkType,
flowName,
methodName,
vendorName,
extras,
journeySource,
});
};

View File

@@ -0,0 +1,39 @@
import { useState } from "react";
import { View } from "react-native";
import BaseBottomSheetComponent from "../../../components/bottomsheet/BaseBottomSheetComponent";
import { ModalView } from "../interface/modals/ModalView";
import { clearBottomSheet, setBottomSheetView } from "../utilities/AlfredUtils";
export const useBottomSheet = () => {
const [bottomsheet, setBottomSheet] = useState<JSX.Element[]>([]);
const onAnimationEndHandler = (id: number | null) => {
if (!id) return;
setBottomSheetView(id);
};
const addBottomSheet = (modalView: ModalView) => {
setBottomSheet((prevState) => [
...prevState,
<View>
<BaseBottomSheetComponent
onBottomSheetAnimationEnd={onAnimationEndHandler}
showModal={true}
onClose={() => removeBottomSheet()}
modalView={modalView}
/>
</View>,
]);
};
const removeBottomSheet = () => {
clearBottomSheet();
setBottomSheet((prevState) => {
const newState = [...prevState];
newState.pop();
return newState;
});
};
return { bottomsheet, addBottomSheet, removeBottomSheet };
};

View File

@@ -0,0 +1,55 @@
import LinearGradient from "react-native-linear-gradient";
import { isValidHexColors } from "../utilities/ValidateColors";
import { Orientation } from "../constants/StringConstant";
export const NaviLinearGradient = ({
gradientColors,
defaultColors,
children,
orientation,
}: {
gradientColors?: string[];
defaultColors?: string[];
children?: React.ReactNode;
orientation?: string;
}) => {
let startValue;
switch (orientation) {
case Orientation.VERTICAL:
startValue = { x: 0.5, y: 0 };
break;
case Orientation.HORIZONTAL:
startValue = { x: 0, y: 0.5 };
break;
case Orientation.DIAGONAL:
startValue = { x: 0, y: 0 };
break;
default:
startValue = { x: 0.5, y: 0 };
break;
}
let endValue;
switch (orientation) {
case Orientation.VERTICAL:
endValue = { x: 0.5, y: 1 };
break;
case Orientation.HORIZONTAL:
endValue = { x: 1, y: 0.5 };
break;
case Orientation.DIAGONAL:
endValue = { x: 1, y: 1 };
break;
default:
endValue = { x: 0.5, y: 1 };
break;
}
return (
<LinearGradient
colors={isValidHexColors(gradientColors, defaultColors)}
start={startValue}
end={endValue}
>
{children}
</LinearGradient>
);
};

View File

@@ -0,0 +1,21 @@
import { useState } from "react";
const useModal = () => {
const [isModalVisible, setModalVisibility] = useState(false);
const toggleModal = () => {
setModalVisibility(!isModalVisible);
};
const handleModalClose = () => {
setModalVisibility(false);
};
return {
isModalVisible,
toggleModal,
handleModalClose,
};
};
export default useModal;

View File

@@ -0,0 +1,5 @@
import * as Sentry from "@sentry/react";
export const logToSentry = (message: string) => {
Sentry.captureException(new Error(message));
};

View File

@@ -0,0 +1,12 @@
import { CtaData } from ".";
export interface BaseScreenProps {
route: RouteParams;
navigation: any;
}
interface RouteParams {
params?: {
ctaData?: CtaData;
};
}

View File

@@ -0,0 +1,181 @@
import { Widget, GenericWidgetData } from "./widgets/Widget";
export interface MemberDetailsRootObject {
applicationResponse: ApplicationResponse;
currentScreenCta: CurrentScreenCta;
currentScreenDefinition: CurrentScreenDefinition;
}
interface CurrentScreenDefinition {
screenData: ScreenData;
}
interface ScreenData {
metaData: ScreenMetaData;
screenStructure: ScreenStructure;
}
interface ScreenStructure {
drawer: Drawer;
footer: Footer;
header: Header;
content: Content;
renderActions: RenderActions;
systemBackCta: SystemBackCta;
floatingActionButton: Drawer;
}
interface SystemBackCta {
actions: Action2[];
}
interface Action2 {
type: string;
ctaData: Cta;
}
interface RenderActions {
preRenderAction: Drawer;
postRenderAction: PostRenderAction;
}
interface PostRenderAction {
actions: Action[];
}
interface Action {
type: string;
isNeededForFirebase: boolean;
isNeededForAppsflyer: boolean;
predefinedEventProperties: PredefinedEventProperty[];
}
interface PredefinedEventProperty {
propertyName: string;
propertyValue: string;
}
interface Content {
widgets: Widget[];
backgroundColor: string;
}
interface InfoDisplayWidgetData extends GenericWidgetData {
cardData?: Container;
titleData?: Banner;
leftIconData?: LeftBottomEndTextIcon;
}
interface NameDobWidget extends GenericWidgetData {
endInput?: EndInput;
labelTitle?: Banner;
startInput?: StartInput;
outputFields?: OutputFields;
}
interface OutputFields {
endInputField: string;
startInputField: string;
}
interface StartInput {
hint: string;
style: Drawer;
keyboardType: string;
}
interface EndInput {
hint: string;
style: Drawer;
endIconUrl: string;
}
interface Header {
widgetId: string;
widgetData: HeaderWidgetData;
widgetType: string;
}
interface HeaderWidgetData {
backIconData: LeftBottomEndTextIcon;
endTitleData: Banner;
middleTitleData: Banner;
}
interface Footer {
widgetId: string;
widgetData: FooterWidgetData;
widgetName: string;
widgetType: string;
}
interface FooterWidgetData {
banner: Banner;
progress: Progress;
rightCta: RightCta;
leftBottomText: Banner;
leftTopStrikedText: Banner;
leftTopUnstrikedText: Banner;
leftBottomEndTextIcon: LeftBottomEndTextIcon;
}
interface LeftBottomEndTextIcon {
style: Drawer;
iconUrl: string;
}
interface RightCta {
title: Banner;
container: Container;
}
interface Container {
style: Drawer;
}
interface Progress {
style: Drawer;
progress: string;
}
interface Banner {
text: string;
style: Drawer;
}
interface Drawer {}
interface CurrentScreenCta {
cta: Cta;
shouldPoll: boolean;
shouldRender: boolean;
screenMetaData: ScreenMetaData;
screenPollingConfigs: ScreenPollingConfigs;
}
interface ScreenPollingConfigs {
initialDelay: number;
interval: number;
numOfRetries: number;
}
interface ScreenMetaData {
screenName: string;
screenType: string;
}
interface Cta {
url: string;
parameters: Parameter[];
}
interface Parameter {
key: string;
value: string;
}
interface ApplicationResponse {
applicationType: string;
applicationId: string;
applicantType: string;
applicationStatus: string;
configVersion: string;
}

View File

@@ -0,0 +1,12 @@
interface AppDowntimeData {
reason?: string | null;
screenName?: string | null;
moduleName?: string | null;
statusCode?: number | null;
networkType?: string | null;
flowName?: string | null;
methodName?: string | null;
vendorName?: string | null;
extras?: Map<string, string> | null;
eventName?: string;
}

View File

@@ -0,0 +1,13 @@
interface GlobalErrorData {
reason?: string | null;
source?: string | null;
moduleName: string;
globalErrorType: string;
statusCode?: number | null;
networkType?: string | null;
flowName?: string | null;
methodName?: string | null;
vendorName?: string | null;
extras?: Map<string, string> | null;
journeySource?: string | null;
}

View File

@@ -0,0 +1,9 @@
import { ViewStyle } from "react-native";
import { PillInfo } from "./PillInfo";
export interface PillData {
id: string;
selectedState?: PillInfo;
unSelectedState?: PillInfo;
defaultPillStyle: ViewStyle;
}

View File

@@ -0,0 +1,7 @@
import { ViewStyle } from "react-native";
import { TextFieldData } from "../widgets/widgetData/TitleWidgetData";
export interface PillInfo {
title?: TextFieldData;
pillStyle: ViewStyle;
}

View File

@@ -0,0 +1,32 @@
export type CtaData = {
url: string;
type?: string;
parameters?: Array<LineItem>;
data?: any;
finish?: boolean;
screenKey?: string;
analyticsEventProperties?: AnalyticsEvent;
};
export type AnalyticsEvent = {
name: string;
properties?: Map<string, string>;
};
type LineItem = {
key?: string;
value?: string | null;
data?: any | null;
};
export interface BaseNavigator {
navigate(ctaData: CtaData): any;
goBack(): any;
}
export enum CtaType {
DEEP_LINK = "DEEP_LINK",
DISMISS_MODAL = "DISMISS_MODAL",
USE_ROOT_DEEPLINK_NAVIGATOR = "USE_ROOT_DEEPLINK_NAVIGATOR",
}

View File

@@ -0,0 +1,10 @@
import { ViewStyle } from "react-native";
import { GenericWidgetData } from "../widgets/Widget";
export interface ModalView {
modalId?: string;
modalData: GenericWidgetData;
modalName: string;
modalType?: string | null;
modalStyle?: ViewStyle;
}

View File

@@ -0,0 +1,24 @@
import { ViewStyle } from "react-native";
export interface Widget {
widgetId: string;
widgetData: GenericWidgetData;
widgetName: string;
widgetType: string | null;
widgetStyle: ViewStyle;
widgetVisibility?: boolean;
}
export interface GenericWidgetData {
widgetOutputDetails?: WidgetOutputData
}
interface WidgetOutputData {
dynamicInputUpdate?: Record<string, any>;
widgetOutput?: WidgetOutput[];
}
interface WidgetOutput {
fieldName?: string;
layoutId?: string;
}

View File

@@ -0,0 +1,23 @@
import { ViewStyle } from "react-native";
import { GenericWidgetData } from "../Widget";
import { ButtonData, CardInfo } from "../widgetData/FooterWithCardWidgetData";
import { ImageFieldData, TextFieldData } from "../widgetData/TitleWidgetData";
export interface PremiumDetailsBottomSheetData extends GenericWidgetData {
title?: TextFieldData;
infoTitle?: TextFieldData;
infoIcon?: ImageFieldData;
detailedPremiumBreakUp?: KeyValueInfoData[];
showDivider?: boolean;
detailedTenureInfo?: KeyValueInfoData[];
calloutCardInfo?: CardInfo;
button?: ButtonData;
viewStyle?: ViewStyle;
}
export interface KeyValueInfoData extends GenericWidgetData {
key?: TextFieldData;
value?: TextFieldData;
displayRightOfKey?: TextFieldData;
displayLeftOfValue?: TextFieldData;
}

View File

@@ -0,0 +1,22 @@
import { ViewStyle } from "react-native";
import { GenericWidgetData } from "../Widget";
import { ButtonData } from "../widgetData/FooterWithCardWidgetData";
import { TextFieldData } from "../widgetData/TitleWidgetData";
import { PillData } from "../../components/PillData";
export interface TitleWithFeedbackPillBottomSheetData
extends GenericWidgetData {
title?: TextFieldData;
feedBackList?: PillData[];
infoCard?: InfoCardData;
leftButton?: ButtonData;
rightButton?: ButtonData;
viewStyle?: ViewStyle;
}
export interface InfoCardData extends GenericWidgetData{
title?: TextFieldData;
subtitle?: TextFieldData;
cardStyle: ViewStyle;
}

View File

@@ -0,0 +1,31 @@
import { GenericWidgetData } from "../Widget";
import { ButtonData } from "../widgetData/FooterWithCardWidgetData";
import { TextFieldData, ImageFieldData } from "../widgetData/TitleWidgetData";
export interface TitleWithStepsBottomSheetData extends GenericWidgetData {
button?: ButtonData;
header?: Header;
steps?: StepData[];
}
export interface Header {
title?: TextFieldData;
subTitle?: TextFieldData;
rightIcon?: ImageFieldData;
backgroundColor?: string;
}
export interface StepData {
serial?: TextFieldData;
image?: ImageFieldData;
title?: TextFieldData;
subTitle?: TextFieldData;
bullets?: Bullet[];
}
export interface Bullet {
bulletIcon?: ImageFieldData;
title?: TextFieldData;
tagTitle?: TextFieldData;
tag?: ImageFieldData;
}

View File

@@ -0,0 +1,10 @@
import { ActionMetaData } from "../../../actions/GenericAction";
import { ButtonData } from "../widgetData/FooterWithCardWidgetData";
import { TextFieldData, TitleWidgetData } from "../widgetData/TitleWidgetData";
export interface ErrorMetaData {
title?: TextFieldData,
subTitle?: TextFieldData,
errorMeta?: ActionMetaData[] | null,
button?: ButtonData
}

View File

@@ -0,0 +1,20 @@
import { ViewStyle } from "react-native";
import { Widget } from "../Widget";
import { ScreenState } from "../../../screen/BaseScreen";
import { ScreenMetaData } from "./ScreenMetaData";
import { GenericActionPayload } from "../../../actions/GenericAction";
export interface ScreenData {
screenStyle?: ViewStyle;
screenId?: string;
screenWidgets?: ScreenWidgets;
screenState?: ScreenState | null;
errorMetaData?: GenericActionPayload;
screenMetaData?: ScreenMetaData;
}
export interface ScreenWidgets {
headerWidgets?: Widget[];
contentWidgets?: Widget[];
footerWidgets?: Widget[];
}

View File

@@ -0,0 +1,6 @@
import { CtaData } from "../..";
export interface ScreenMetaData {
backButtonCta?: CtaData;
redirectionCta?: CtaData;
}

View File

@@ -0,0 +1,10 @@
import { GenericWidgetData } from "../Widget";
import { ImageFieldData, TitleWidgetData } from "./TitleWidgetData";
export interface ComparisonWidgetData extends GenericWidgetData {
leftIcon?: ImageFieldData;
leftTitle?: TitleWidgetData;
rightIcon?: ImageFieldData;
rightTitle?: TitleWidgetData;
divider?: ImageFieldData;
}

View File

@@ -0,0 +1,15 @@
import { CtaData } from "../..";
import { GenericWidgetData } from "../Widget";
import { LottieFieldData } from "./TitleWidgetData";
export interface FabWidgetData extends GenericWidgetData {
lottieData?: LottieFieldData;
properties?: FabProperties;
callbackCta?: CtaData;
};
export interface FabProperties {
isDraggable?: boolean;
startingPosition?: number;
}

View File

@@ -0,0 +1,40 @@
import { ViewStyle } from "react-native";
import { CtaData } from "../..";
import { SumInsuredRequestData } from "../../../../Container/Navi-Insurance/network/QuotePageApi";
import { GenericActionPayload } from "../../../actions/GenericAction";
import { GenericWidgetData } from "../Widget";
import { TextFieldData } from "./TitleWidgetData";
export interface FooterWithCardWidgetData extends GenericWidgetData {
title?: TextFieldData;
subtitle?: TextFieldData;
cardInfo?: CardInfo;
footerButton?: ButtonData;
cardAction?: GenericActionPayload;
titleAction?: GenericActionPayload;
action?: GenericActionPayload;
buttonAction?: GenericActionPayload;
}
export interface FinalPatchCallRequestBody {
requestData: SumInsuredRequestData;
nextPageCta: CtaData;
}
export interface CardInfo extends GenericWidgetData {
title?: TextFieldData;
rightTitle?: TextFieldData;
}
export interface ButtonData extends GenericWidgetData {
title?: TextFieldData;
state?: ButtonState;
buttonStyle?: ViewStyle;
cta?: CtaData;
}
export enum ButtonState {
ENABLED = "ENABLED",
DISABLED = "DISABLED",
LOADING = "LOADING",
}

View File

@@ -0,0 +1,21 @@
import { ViewStyle } from "react-native";
import { CtaData } from "../..";
import { GenericWidgetData } from "../Widget";
import { ImageFieldData, TextFieldData } from "./TitleWidgetData";
export interface GridWithCardWidgetData extends GenericWidgetData {
title?: TextFieldData;
subtitle?: TextFieldData;
rightTitle?: TextFieldData;
gridItems?: GridCardItemData[];
numColumns?: number;
}
export interface GridCardItemData {
id?: string;
image?: ImageFieldData;
title?: TextFieldData;
cardStyle?: ViewStyle;
cta?: CtaData;
}

View File

@@ -0,0 +1,11 @@
import { CtaData } from "../..";
import { GenericWidgetData } from "../Widget";
import { ImageFieldData, LottieFieldData, TextFieldData } from "./TitleWidgetData";
export interface HeaderLottieAnimationWidgetData extends GenericWidgetData {
backgroundLottie?: LottieFieldData;
title?: TextFieldData;
cta?: CtaData;
backgroundGradient?: string[];
gradientOrientation?: string;
}

View File

@@ -0,0 +1,16 @@
import { GenericActionPayload } from "../../../actions/GenericAction";
import { GenericWidgetData } from "../Widget";
import {
ImageFieldData,
LottieFieldData,
TextFieldData,
} from "./TitleWidgetData";
export interface HeaderWithAssetsWidgetData extends GenericWidgetData {
leftIcon?: ImageFieldData;
leftLottie?: LottieFieldData;
centerTitle?: TextFieldData;
rightIcon?: ImageFieldData;
rightLottie?: LottieFieldData;
action?: GenericActionPayload;
}

View File

@@ -0,0 +1,13 @@
import { ViewStyle } from "react-native";
import { GenericWidgetData } from "../Widget";
import { GenericActionPayload } from "../../../actions/GenericAction";
export interface SliderWidgetData extends GenericWidgetData {
sliderStyle?: ViewStyle;
minimumValue?: number;
maximumValue?: number;
// Used for demo the widget structure can be refined below is just sample.
// TODO: Raaj/Himanshu for cleanup
onValueChangeAction?: GenericActionPayload;
onSliderReleaseActionSequence?: GenericActionPayload;
}

View File

@@ -0,0 +1,27 @@
import { GenericActionPayload } from "../../../actions/GenericAction";
import { GenericWidgetData } from "../Widget";
import { TextFieldData } from "./TitleWidgetData";
export interface SumInsuredWidgetData extends GenericWidgetData {
carouselListData?: SumInsuredData[];
carouselStyles?: {
selectedStyles?: any;
unselectedStyles?: any;
};
widgetMetaData? : {
selectedItemIndex?: number;
recommendItemIndex?: number;
selectedItemTagText? : string;
onValueChangeAction? : GenericActionPayload
onCarouselReleaseActionSequence? : GenericActionPayload;
}
}
export interface SumInsuredData {
itemId?: string;
sumInsured?: string;
title?: TextFieldData;
subtitle?: TextFieldData;
isFirst?: boolean;
isLast?: boolean;
dependentWidgets?: any;
}

View File

@@ -0,0 +1,16 @@
import { GenericActionPayload } from "../../../actions/GenericAction";
import { GenericWidgetData } from "../Widget";
import {
ImageFieldData,
LottieFieldData,
TextFieldData,
} from "./TitleWidgetData";
export interface TitleSubtitleWithAssetWidgetData extends GenericWidgetData {
title?: TextFieldData;
subtitle?: TextFieldData;
image?: ImageFieldData;
action?: GenericActionPayload;
backgroundImage?: ImageFieldData;
backgroundLottie?: LottieFieldData;
}

View File

@@ -0,0 +1,49 @@
import { ImageStyle, TextStyle } from "react-native";
import { CtaData } from "../..";
import { GenericWidgetData } from "../Widget";
export interface TitleWidgetData extends GenericWidgetData {
title?: TextFieldData;
subtitle?: TextFieldData;
rightTitle?: TextFieldData;
cta?: CtaData;
}
export interface TextFieldData {
text: string;
textStyle?: TextStyle;
substringStyles?: SubstringStyle[];
textDrawableData?: TextDrawableData;
cta?: CtaData;
}
export interface SubstringStyle {
substring: string;
textStyle?: TextStyle;
}
export interface TextDrawableData {
left?: ImageFieldData;
right?: ImageFieldData;
top?: ImageFieldData;
bottom?: ImageFieldData;
leftLottie?: LottieFieldData;
rightLottie?: LottieFieldData;
topLottie?: LottieFieldData;
bottomLottie?: LottieFieldData;
}
export interface ImageFieldData {
url: string;
imageStyle?: ImageStyle;
cta?: CtaData;
}
export interface LottieFieldData {
url: string;
loop?: boolean;
autoPlay?: boolean;
lottieStyle?: ImageStyle;
cta?: CtaData;
delayAnimationBy?: number;
}

View File

@@ -0,0 +1,16 @@
import { CtaData } from "../..";
import { GenericWidgetData } from "../Widget";
import {
ImageFieldData,
LottieFieldData,
TextFieldData,
} from "./TitleWidgetData";
export interface TitleWithAssetsWidgetData extends GenericWidgetData {
leftIcon?: ImageFieldData;
leftLottie?: LottieFieldData;
title?: TextFieldData;
rightIcon?: ImageFieldData;
rightLottie?: LottieFieldData;
cta?: CtaData;
}

View File

@@ -0,0 +1,17 @@
import { TextStyle } from "react-native";
import { GenericWidgetData } from "../Widget";
import { TextFieldData } from "./TitleWidgetData";
export interface TitleWithListWidgetData extends GenericWidgetData {
title?: TextFieldData;
rightTitle?: TextFieldData;
listData?: ListItem[];
}
export interface ListItem extends GenericWidgetData {
id: string;
title?: TextFieldData;
rightTitle?: TextFieldData;
}

View File

@@ -0,0 +1,58 @@
import React from "react";
import { View, ViewStyle } from "react-native";
import TitleWithFeedbackPillBottomSheet from "../../../components/bottomsheet/title-with-feed-back-bottom-sheet/TitleWithFeedBackBottomSheet";
import PremiumDetailsBottomSheet from "../../../components/bottomsheet/title-with-list-bottom-sheet/TitleWithListBottomSheet";
import TitleWithStepsBottomSheet from "../../../components/bottomsheet/title-with-steps-bottom-sheet/TitleWithStepsBottomSheet";
import {
PREMIUM_DETAILS_BOTTOM_SHEET,
TITLE_WITH_FEEDBACK_PILL_BOTTOM_SHEET,
TITLE_WITH_STEPS_BOTTOM_SHEET,
} from "../constants/ModalNameConstants";
import { CtaData } from "../interface";
import { ModalView } from "../interface/modals/ModalView";
import { GenericWidgetData } from "../interface/widgets/Widget";
export const GetModalView = {
getModal: (
modal: ModalView,
handleModalClick: (cta: CtaData) => void
): JSX.Element => {
const { modalName, modalData, modalStyle } = modal;
return resolveModalView(modalName, modalData, modalStyle, handleModalClick);
},
};
function resolveModalView(
modalName: string,
modalData: GenericWidgetData,
modalStyle: ViewStyle | undefined,
handleModalClick: (cta: CtaData) => void
) {
switch (modalName) {
case PREMIUM_DETAILS_BOTTOM_SHEET:
return (
<PremiumDetailsBottomSheet
bottomSheetData={modalData}
handleModalClick={handleModalClick}
/>
);
case TITLE_WITH_FEEDBACK_PILL_BOTTOM_SHEET:
return (
<TitleWithFeedbackPillBottomSheet
bottomSheetData={modalData}
handleModalClick={handleModalClick}
/>
);
case TITLE_WITH_STEPS_BOTTOM_SHEET:
return (
<TitleWithStepsBottomSheet
bottomSheetData={modalData}
handleModalClick={handleModalClick}
/>
);
default:
return <View />;
}
}

View File

@@ -0,0 +1,11 @@
import { NativeModules } from "react-native";
const {
NativeDeeplinkNavigatorModule,
PreferenceManagerConnector,
NetworkConnectorModule,
AlfredModuleConnector,
NativeAnalyticsModule
} = NativeModules;
export { NativeDeeplinkNavigatorModule, PreferenceManagerConnector, NetworkConnectorModule, AlfredModuleConnector, NativeAnalyticsModule };

View File

@@ -0,0 +1,71 @@
import { createNavigationContainerRef } from "@react-navigation/native";
import { BaseNavigator, CtaData } from "../interface";
import { BASE_SCREEN } from "../constants/ScreenNameConstants";
import {
BaseActionTypes,
GenericActionPayload,
ActionMetaData,
} from "../actions/GenericAction";
import WidgetActionHandler from "../widgets/widget-actions/WidgetActionHandler";
import { ScreenActionHandler } from "../screen/ScreenActionHandler";
import { logToSentry } from "../hooks/useSentryLogging";
export const navigationRef = createNavigationContainerRef();
export const CtaNavigator: BaseNavigator = {
navigate: (ctaData: CtaData) => {
if (navigationRef.isReady()) {
navigationRef.navigate(BASE_SCREEN, { ctaData });
}
},
goBack: () => {
if (navigationRef.isReady()) {
navigationRef.goBack();
}
},
};
export const Router = {
handleAction(actionPayload: GenericActionPayload, navigation?: any): void {
switch (actionPayload.baseActionType) {
case BaseActionTypes.WIDGET_ACTION: {
actionPayload.metaData?.forEach(
(widgetMetaData: ActionMetaData, _: number) => {
if (!!actionPayload.setScreenData) {
WidgetActionHandler.handleWidgetAction(
!!widgetMetaData ? widgetMetaData : {},
actionPayload.setScreenData,
actionPayload.setErrorMetaData,
actionPayload.screenData,
actionPayload.ctaData,
navigation
);
}
}
);
break;
}
case BaseActionTypes.SCREEN_ACTION: {
actionPayload.metaData?.forEach(
(screenMetaData: ActionMetaData, _: number) => {
if (!!actionPayload.setScreenData) {
ScreenActionHandler.handleScreenAction(
!!screenMetaData ? screenMetaData : {},
actionPayload.setScreenData,
actionPayload.screenData,
navigation
);
}
}
);
break;
}
default: {
logToSentry(
`Invalid baseActionType ${actionPayload.baseActionType} | MethodName: Router.handleAction`
);
break;
}
}
},
};

View File

@@ -0,0 +1,64 @@
import IntroScreen from "../../Container/Navi-Insurance/screen/IntroScreen";
import {
BASE_SCREEN,
INSURANCE_LANDING_PAGE_SCREEN,
INTRO_SCREEN,
} from "../constants/ScreenNameConstants";
import { CtaData } from "../interface";
import { ThemeProvider } from "../../../components/ThemeContext";
import { LogBox } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { Provider } from "react-redux";
import reduxStore from "../redux/store";
import { PersistGate } from "redux-persist/integration/react";
import BaseScreen from "../screen/BaseScreen";
import { navigationRef } from "./NavigationRouter";
LogBox.ignoreLogs(["Warning: ..."]); // Ignore log notification by message
LogBox.ignoreAllLogs(); //Ignore all log notifications
const Stack = createStackNavigator();
export const RnApp = {
create: (ctaData: CtaData) => {
const { store, persistor } = reduxStore();
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider>
<NavigationContainer ref={navigationRef}>
{getScreenStack(ctaData)}
</NavigationContainer>
</ThemeProvider>
</PersistGate>
</Provider>
);
},
};
function getScreenStack(ctaData: CtaData) {
return (
<Stack.Navigator
initialRouteName={BASE_SCREEN}
screenOptions={{
headerShown: false,
gestureEnabled: false,
}}
>
<Stack.Screen
name={INTRO_SCREEN}
component={IntroScreen}
initialParams={{ ctaData: ctaData }}
/>
<Stack.Screen
name={BASE_SCREEN}
component={BaseScreen}
initialParams={{ ctaData: ctaData }}
/>
</Stack.Navigator>
);
}
export default RnApp;

View File

@@ -0,0 +1 @@
export * from "./screens/screenActionCreators";

View File

@@ -0,0 +1,8 @@
import { combineReducers } from "redux";
import screenReducer from "./screens/screenReducer";
const rootReducer = combineReducers({
screenReducer: screenReducer,
});
export default rootReducer;

View File

@@ -0,0 +1,7 @@
import { CtaData } from "../../../interface";
import { ScreenState } from "../../../screen/BaseScreen";
export interface UpdateCtaDataPayload {
cta: CtaData;
setScreenState?: ScreenState | null;
}

View File

@@ -0,0 +1,10 @@
import { UpdateCtaDataPayload } from "./action-interfaces/UpdateCtaPayload";
import { UPDATE_CTA_DATA } from "./screenReducerActionTypes";
//action creators
export const updateCtaData = (payload: UpdateCtaDataPayload) => {
return {
type: UPDATE_CTA_DATA,
payload: payload,
};
};

View File

@@ -0,0 +1,26 @@
import { ScreenState } from "../../screen/BaseScreen";
import { UPDATE_CTA_DATA } from "./screenReducerActionTypes";
const initialState = {
ctaData: null,
screenState: ScreenState.LOADING,
};
const screenReducer = (
state = initialState,
action: { type: string; payload: any }
) => {
switch (action.type) {
case UPDATE_CTA_DATA:
return {
...state,
ctaData: action.payload.cta,
screenState: ScreenState.LOADING,
};
default:
return state;
}
};
export default screenReducer;

View File

@@ -0,0 +1 @@
export const UPDATE_CTA_DATA = "UPDATE_CTA_DATA";

28
App/common/redux/store.js Normal file
View File

@@ -0,0 +1,28 @@
import { applyMiddleware, configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";
import thunk from "redux-thunk";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { persistReducer, persistStore } from "redux-persist";
const middleWares = [getDefaultMiddleware({
thunk: true,
serializableCheck: false
}
)];
const persistConfig = {
key: "root",
storage: AsyncStorage,
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export default () => {
//by default configureStore has middleware thunk in it, it is added explicitly below for ref to add other middlewares
let store = configureStore(
{ reducer: persistedReducer },
applyMiddleware(...middleWares)
);
let persistor = persistStore(store);
return { store, persistor };
};

View File

@@ -0,0 +1,174 @@
import { useFocusEffect } from "@react-navigation/native";
import { isEqual } from "lodash";
import React, { useEffect, useMemo, useState } from "react";
import { View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { commonStyles } from "../../Container/Navi-Insurance/Styles";
import { ActionMetaData, GenericActionPayload } from "../actions/GenericAction";
import { sendAsAnalyticsEvent } from "../hooks/useAnalyticsEvent";
import { useBottomSheet } from "../hooks/useBottomSheet";
import { CtaData } from "../interface";
import { ModalView } from "../interface/modals/ModalView";
import { ScreenData } from "../interface/widgets/screenData/ScreenData";
import { Router } from "../navigator/NavigationRouter";
import { updateCtaData } from "../redux/screens/screenActionCreators";
import {
getCacheKey,
getScreenDataFromCache,
isScreenWhiteListedForCaching,
saveScreenDataInCache,
} from "../utilities/CacheUtils";
import { getScreenNameFromCtaData } from "../utilities/MiscUtils";
import { WidgetActionTypes } from "../widgets/widget-actions/WidgetActionTypes";
import { ScreenMapper } from "./screen-mappers/ScreenMapper";
import { logToSentry } from "../hooks/useSentryLogging";
const BaseScreen: React.FC<{ navigation: any; route: any }> = ({
navigation,
route,
}) => {
const [screenData, setScreenData] = useState<ScreenData | null>(null);
const [screenName, setScreenName] = useState<string | null>(null);
const [screenKey, setScreenKey] = useState<string | null>(null);
const [screenState, setScreenState] = useState<ScreenState | null>(
ScreenState.LOADING
);
const [errorMetaData, setErrorMetaData] = useState<ActionMetaData[] | null>(
null
);
useEffect(() => {
const cacheKey = getCacheKey(screenName, screenKey);
if (!screenData) {
const screenInitialData: ScreenData = {
screenState: ScreenState.LOADING,
};
setScreenData(screenInitialData);
}
if (!!cacheKey && isScreenWhiteListedForCaching(screenName)) {
if (
!(
!!screenData?.errorMetaData ||
screenData?.screenState === ScreenState.ERROR
)
) {
saveScreenDataInCache(screenName, screenData);
}
}
}, [screenData]);
useEffect(() => {
if (!!getScreenNameFromCtaData(route.params?.ctaData)) {
setScreenName(getScreenNameFromCtaData(route.params?.ctaData)!!);
if (!!route.params?.ctaData?.screenKey) {
setScreenKey(route.params?.ctaData?.screenKey);
}
if (isScreenWhiteListedForCaching(screenName)) {
retrieveScreenDataFromCache(
getScreenNameFromCtaData(route.params?.ctaData)!!,
route.params?.ctaData?.screenKey
);
}
}
}, []);
const dispatch = useDispatch();
const retrieveScreenDataFromCache = (
screenName: string | null | undefined,
screenKey: string | null | undefined
) => {
let cacheKey = getCacheKey(screenName, screenKey);
if (!!cacheKey) {
getScreenDataFromCache(cacheKey).then((screenData) => {
if (!!screenData) {
setScreenData(screenData);
}
});
}
};
const { cta } = useSelector((state: any) => {
const savedCta = state.screenReducer.ctaData;
if (isEqual(savedCta, route.params.ctaData)) {
return { cta: state.screenReducer.ctaData };
}
return { cta: route.params.ctaData };
});
useEffect(() => {
const ctaData: CtaData = route.params.ctaData;
if (!isEqual(cta, ctaData)) {
dispatch(updateCtaData({ cta: ctaData, setScreenState: screenState }));
}
}, [route.params.ctaData, screenState]);
useFocusEffect(
React.useCallback(() => {
// This function will be called when the screen is focused (resumed)
console.log("Screen resumed");
// add code here to perform actions on screen resume
// update redux store based on current screen name
return () => {
// This function will be called when the screen loses focus (paused)
console.log("Screen paused");
};
}, [])
);
const { bottomsheet, addBottomSheet } = useBottomSheet();
const handleActions = (actionPayload?: GenericActionPayload) => {
actionPayload?.metaData?.forEach((ActionMetaData) => {
if (!!ActionMetaData.analyticsEventProperties) {
sendAsAnalyticsEvent(ActionMetaData.analyticsEventProperties);
}
if (ActionMetaData.actionType === WidgetActionTypes.OPEN_BOTTOM_SHEET) {
addBottomSheet(ActionMetaData.data as ModalView);
} else {
const updatedActionPayload: GenericActionPayload = {
...(actionPayload as GenericActionPayload),
setScreenData,
setScreenState,
setErrorMetaData,
ctaData: cta,
screenData: { ...screenData },
};
if (!!actionPayload) {
Router.handleAction(updatedActionPayload, navigation);
} else {
// handle error
logToSentry(
`Action payload is missing or invalid: ${actionPayload} | MethodName: handleActions`
);
}
}
});
};
const MemoizedScreenMapper = useMemo(() => {
return (
<View style={commonStyles.flex_1}>
{ScreenMapper.getScreenMapper(cta, screenData, handleActions)}
</View>
);
}, [cta, screenData]);
return (
<View style={commonStyles.flex_1}>
{MemoizedScreenMapper}
{bottomsheet.map((sheet, index) => (
<React.Fragment key={`bottomSheet-${index}`}>{sheet}</React.Fragment>
))}
</View>
);
};
export enum ScreenState {
LOADING,
LOADED,
ERROR,
OVERLAY,
}
export default BaseScreen;

View File

@@ -0,0 +1,109 @@
import { ActionMetaData, BaseActionTypes } from "../actions/GenericAction";
import { ScreenData } from "../interface/widgets/screenData/ScreenData";
import { ScreenActionTypes } from "./ScreenActionTypes";
import { Dispatch, SetStateAction } from "react";
import { post, get } from "../../../network/NetworkService";
import { CtaData } from "../interface";
import { getXTargetHeaderInfo } from "../../../network/ApiClient";
import { GI } from "../constants/NavigationHandlerConstants";
import { ScreenState } from "./BaseScreen";
import { BASE_SCREEN } from "../constants/ScreenNameConstants";
import { logToSentry } from "../hooks/useSentryLogging";
export const ScreenActionHandler = {
handleScreenAction: (
screenMetaData: ActionMetaData,
setScreenData: Dispatch<SetStateAction<ScreenData | null>>,
screenData?: ScreenData | null,
navigation?: any
) => {
switch (screenMetaData.actionType) {
case ScreenActionTypes.FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND: {
return get<ApiResponse<CtaData>>(
`v3/quotes/${screenMetaData.data.quoteId}`,
getXTargetHeaderInfo(GI.toLocaleUpperCase())
)
.then((response) => {
const updatedScreenData: ScreenData = {
...(response.data as ScreenData),
screenState: ScreenState.LOADED,
};
setScreenData(updatedScreenData);
return;
})
.catch((error) => {
logToSentry(
`No response from api call: ${screenMetaData.actionType} | Error: ${error} | MethodName: handleScreenAction`
);
const updatedScreenData: ScreenData = {
screenState: ScreenState.ERROR,
errorMetaData: {
baseActionType: BaseActionTypes.SCREEN_ACTION,
metaData: [
{
actionType: ScreenActionTypes.SHOW_LOADER,
},
{
data: screenMetaData.data,
actionType:
ScreenActionTypes.FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND,
},
],
},
};
setScreenData(updatedScreenData);
});
}
case ScreenActionTypes.FETCH_QUOTE_V3: {
return post<ApiResponse<CtaData>>(
"v3/quotes",
screenMetaData.data,
getXTargetHeaderInfo(GI.toLocaleUpperCase())
)
.then((response) => {
console.log("Quote_v3_Call success", response.data);
if (screenData?.screenState) {
screenData.screenState = ScreenState.LOADING;
}
if (response.data) {
const cta = response.data as CtaData;
// By default routing is happening via Base screen hence below navigate sends the cta there to get routed to specific screenName based on Cta
navigation.navigate(BASE_SCREEN, { ctaData: cta });
}
})
.catch((error) => {
logToSentry(
`No response from api call: ${screenMetaData.actionType} | Error: ${error} | MethodName: handleScreenAction}`
);
const updatedScreenData: ScreenData = {
screenState: ScreenState.ERROR,
errorMetaData: {
baseActionType: BaseActionTypes.SCREEN_ACTION,
metaData: [
{
actionType: ScreenActionTypes.SHOW_LOADER,
},
{
data: screenMetaData.data,
actionType: ScreenActionTypes.FETCH_QUOTE_V3,
},
],
},
};
setScreenData(updatedScreenData);
return;
});
}
case ScreenActionTypes.SHOW_LOADER: {
const updatedScreenData: ScreenData = {
...screenData,
screenState: ScreenState.LOADING,
};
setScreenData(updatedScreenData);
return;
}
default:
return;
}
},
};

View File

@@ -0,0 +1,6 @@
export const ScreenActionTypes = {
FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND:
"FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND",
FETCH_QUOTE_V3: "FETCH_QUOTE_V3",
SHOW_LOADER: "SHOW_LOADER",
};

View File

@@ -0,0 +1,79 @@
import { View } from "react-native";
import {
IntroScreen,
InsuranceLandingPageScreen,
QuoteOfferScreen,
} from "../../../Container/Navi-Insurance";
import { getScreenNameFromCtaData } from "../../utilities/MiscUtils";
import { GenericActionPayload } from "../../actions/GenericAction";
import { CtaData } from "../../interface";
import { ScreenData } from "../../interface/widgets/screenData/ScreenData";
import {
INSURANCE_LANDING_PAGE_SCREEN,
INTRO_SCREEN,
QUOTE_OFFER_SCREEN,
BUY_INSURANCE_SCREEN,
QUOTE_APOLOGY_SCREEN,
} from "../../constants/ScreenNameConstants";
import QuoteApologyScreen from "../../../Container/Navi-Insurance/screen/quote-apology-screen/QuoteApologyScreen";
import { logToSentry } from "../../hooks/useSentryLogging";
export const GIScreenMapper = {
getScreen(
ctaData: CtaData | null | undefined,
screenData: ScreenData | null,
handleActions: (actionPayload?: GenericActionPayload) => void
): JSX.Element {
if (!!ctaData) {
const thirdIdentifier = getScreenNameFromCtaData(ctaData);
switch (thirdIdentifier) {
case INTRO_SCREEN:
return <IntroScreen ctaData={ctaData} />;
case QUOTE_OFFER_SCREEN:
case BUY_INSURANCE_SCREEN:
return (
<QuoteOfferScreen
ctaData={ctaData}
screenData={screenData}
handleActions={handleActions}
/>
);
case QUOTE_APOLOGY_SCREEN:
return (
<QuoteApologyScreen
ctaData={ctaData}
screenData={screenData}
handleActions={handleActions}
/>
);
case INSURANCE_LANDING_PAGE_SCREEN:
return (
<InsuranceLandingPageScreen
ctaData={ctaData}
screenData={screenData}
handleActions={handleActions}
/>
);
default: {
let ctaDataa: CtaData = {
url: "react/insurance_landing_page",
screenKey: "insurance_lp_1",
};
return (
<InsuranceLandingPageScreen
ctaData={ctaDataa}
screenData={screenData}
handleActions={handleActions}
/>
);
}
//default will be changed to cta handler through bridge
}
} else {
logToSentry(
`CtaData is missing or invalid: ${ctaData} | MethodName: GIScreenMapper.getScreen`
);
return <View />;
}
},
};

View File

@@ -0,0 +1,37 @@
import { View } from "react-native";
import { CtaData } from "../../interface";
import { GenericActionPayload } from "../../actions/GenericAction";
import { ScreenData } from "../../interface/widgets/screenData/ScreenData";
import { getScreenMapperNameFromCtaData } from "../../utilities/MiscUtils";
import { commonStyles } from "../../../Container/Navi-Insurance/Styles";
import { GIScreenMapper } from "./GIScreenMapper";
import { GI } from "../../constants/NavigationHandlerConstants";
import { logToSentry } from "../../hooks/useSentryLogging";
export const ScreenMapper = {
getScreenMapper(
ctaData: CtaData | null | undefined,
screenData: ScreenData | null,
handleActions: (actionPayload?: GenericActionPayload) => void
): JSX.Element {
if (!!ctaData) {
console.log("ScreenMapper", ctaData);
const secondIdentifier = getScreenMapperNameFromCtaData(ctaData);
switch (secondIdentifier) {
case GI:
return (
<View style={commonStyles.flex_1}>
{GIScreenMapper.getScreen(ctaData, screenData, handleActions)}
</View>
);
default:
return <View />;
}
} else {
logToSentry(
`CtaData is missing or invalid: ${ctaData} | MethodName: ScreenMapper.getScreen`
);
return <View />;
}
},
};

View File

@@ -0,0 +1,29 @@
import { StyleSheet } from "react-native";
const styles = StyleSheet.create({
modal: {
justifyContent: "flex-end",
margin: 0,
},
modalContent: {
backgroundColor: "white",
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
overflow: "hidden",
},
barContainer: {
alignItems: "center",
paddingVertical: 10,
},
barIcon: {
width: 60,
height: 5,
backgroundColor: "#bbb",
borderRadius: 3,
},
});
export default styles;

View File

@@ -0,0 +1,6 @@
import { AlfredModuleConnector } from "../native-module/NativeModules";
export const setBottomSheetView = (id: number | null) =>
AlfredModuleConnector.setBottomSheetView(id!!);
export const clearBottomSheet = () => AlfredModuleConnector.clearBottomSheet();

View File

@@ -0,0 +1,94 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { BUY_INSURANCE_SCREEN, QUOTE_OFFER_SCREEN } from "../constants/ScreenNameConstants";
import { ScreenData } from "../interface/widgets/screenData/ScreenData";
import { NetworkConnectorModule } from "../../common/native-module/NativeModules";
import { BUILD_CONFIG_DETAILS } from "../constants/StringConstant";
export const getScreenDataFromCache = async (
key: string
): Promise<ScreenData | null> => {
try {
const value = await AsyncStorage.getItem(key);
if (!!value) {
// The key exists in AsyncStorage
const deserializedObject = JSON.parse(value);
try {
return deserializedObject as ScreenData;
} catch (error) {
return null;
}
} else {
return null;
}
} catch (error) {
console.error("Error checking AsyncStorage:", error);
return null;
}
};
export const saveScreenDataInCache = async (
key: string | null,
data: ScreenData | null
) => {
if (!!key && !!data) {
const serializedObject = JSON.stringify(data);
await AsyncStorage.setItem(key, serializedObject);
} else {
console.error("key or data was invalid");
}
};
export const getCacheKey = (
screenName: string | null | undefined,
screenKey: string | null | undefined
) => {
if (!!screenKey && !!screenName) {
return screenName + "#" + screenKey;
} else {
return screenName;
}
};
export const setBuildConfigDetails = async () => {
let buildConfigData: string | undefined
await NetworkConnectorModule.getBuildConfigDetails().then((response: string) => {
buildConfigData = response
});
if (buildConfigData) {
await AsyncStorage.setItem(BUILD_CONFIG_DETAILS, buildConfigData);
}
}
export const getBuildConfigDetails = async (
): Promise<BuildConfigDetails | null> => {
const value = await AsyncStorage.getItem(BUILD_CONFIG_DETAILS);
try {
if (!!value) {
// The key exists in AsyncStorage
const buildConfigData = JSON.parse(value);
try {
return buildConfigData as BuildConfigDetails;
} catch (error) {
return null;
}
} else {
return null;
}
} catch (error) {
// Sentry log -> Error checking BuildConfigDetails
return null;
}
}
export const isScreenWhiteListedForCaching = (
screenName: string | null | undefined
) => {
console.log(
"Caching eligibilty",
screenName,
!screensWithCachingDisabled.includes(screenName || "")
);
return !screensWithCachingDisabled.includes(screenName || "");
};
export const screensWithCachingDisabled = [BUY_INSURANCE_SCREEN, QUOTE_OFFER_SCREEN];

View File

@@ -0,0 +1,13 @@
import { QUOTE_ID } from "../constants/StringConstant";
import { CtaData } from "../interface";
export const getQuoteIdFromCta = (ctaData?: CtaData) => {
let quoteId;
ctaData?.parameters?.forEach((item) => {
if (item.key === QUOTE_ID) {
quoteId = item.value;
return;
}
});
return quoteId;
};

View File

@@ -0,0 +1,54 @@
import { CtaData } from "../interface";
import { logToSentry } from "../hooks/useSentryLogging";
export function getScreenNameFromCtaData(ctaData: CtaData): string | undefined {
const splitDeeplink = ctaData.url?.split("/");
return splitDeeplink?.at(2);
}
export function getScreenMapperNameFromCtaData(
ctaData: CtaData
): string | undefined {
const splitDeeplink = ctaData.url?.split("/");
return splitDeeplink?.at(1);
}
export function updateValueByKeyPath<T>(
obj: T,
keyPath?: string,
newValue?: any
): void {
if (!keyPath || !newValue) {
return;
}
const keys: string[] = keyPath.split(".");
let currentObj: any = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key: string = keys[i] || "";
if (
!!key &&
currentObj.hasOwnProperty(key) &&
typeof currentObj[key] === "object"
) {
currentObj = currentObj[key];
} else {
logToSentry(
`Key path not valid: ${keyPath} | Faulty key: ${key} | MethodName: ${arguments.callee.name}`
);
return;
}
}
const lastKey: string = keys[keys.length - 1] || "";
if (!!lastKey && currentObj.hasOwnProperty(lastKey)) {
//the values here should be in string format
try {
currentObj[lastKey] = newValue;
} catch (exception) {
logToSentry(
`Key path not valid: ${keyPath} | Faulty key: ${lastKey} | MethodName: ${arguments.callee.name}`);
}
}
}

View File

@@ -0,0 +1,16 @@
const mockResponse = require("../../../assets/mocks/mockApiResponse.json");
// Function to simulate a mock API call
export function mockApiCall<T>(shouldSucceed: boolean): Promise<T> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
// Simulate a successful API response
resolve(mockResponse as T);
} else {
// Simulate an error response
reject(new Error("API request failed"));
}
}, 2000); // Simulate a 2-second delay
});
}

View File

@@ -0,0 +1,7 @@
export const useAsRecord = (map?: Map<string, string> | null) => {
const myRecord: Record<string, string> = {};
map?.forEach((value, key) => {
myRecord[key] = value;
});
return myRecord;
};

View File

@@ -0,0 +1,43 @@
import { logToSentry } from "../hooks/useSentryLogging";
export function parseValue(value: any, targetType: string): any | null {
if (targetType === "string") {
return String(value);
} else if (targetType === "number") {
return Number(value);
} else if (targetType === "object") {
if (typeof value === "string") {
try {
return JSON.parse(value);
} catch (error) {
logToSentry(
`Error parsing JSON: ${value} | Error: ${error} | MethodName: ${arguments.callee.name}`
);
return null;
}
} else {
logToSentry(
`Value is not a string; cannot parse as object: ${value} | MethodName: ${arguments.callee.name}`
);
return null;
}
} else {
logToSentry(
`Invalid targetType: ${targetType} | MethodName: ${arguments.callee.name}`
);
return null;
}
}
// Usage examples:
// const stringValue: string = '123';
// const numberValue: number = 456;
// const objectValue: string = '{"key": "value"}';
// const parsedString: string = parseValue(stringValue, 'string');
// const parsedNumber: number = parseValue(numberValue, 'number');
// const parsedObject: object | null = parseValue(objectValue, 'object');
// console.log(parsedString); // "123"
// console.log(parsedNumber); // 456
// console.log(parsedObject); // { key: 'value' }

View File

@@ -0,0 +1,41 @@
import { logToSentry } from "../hooks/useSentryLogging";
import { PreferenceManagerConnector } from "../native-module/NativeModules";
export const getStringPreference = async (
key: string,
type: string = "string"
) => {
try {
const data = await PreferenceManagerConnector.get(key, type);
return data;
} catch (error) {
logToSentry(
`Error getting data for key: ${key}, type: ${type} | Error: ${error} | MethodName: getStringPreference`
);
return null;
}
};
export const setStringPreference = async (key: string, preferenceData: any) => {
try {
const data = await PreferenceManagerConnector.set(key, preferenceData);
return data;
} catch (error) {
logToSentry(
`Error setting data for key: ${key}, type: ${preferenceData} | Error: ${error} | MethodName: setStringPreference`
);
return null;
}
};
export const getIntPreference = async (key: string, type: string = "int") => {
try {
const data = await PreferenceManagerConnector.get(key, type);
return data;
} catch (error) {
logToSentry(
`Error getting data for key: ${key}, type: ${type} | Error: ${error} | MethodName: getIntPreference`
);
return null;
}
};

View File

@@ -0,0 +1,15 @@
import { PixelRatio } from "react-native";
import { logToSentry } from "../hooks/useSentryLogging";
export const getSizeInPx = (size: number) => {
return PixelRatio.getPixelSizeForLayoutSize(size);
};
export const getIndexFromOffset = (offset: number, SIZE: number) => {
if (SIZE === 0) {
logToSentry(
`Division by zero, offset: ${offset} | MethodName: getIndexFromOffset`
);
}
return Math.abs(Math.round(offset / SIZE));
};

View File

@@ -0,0 +1,15 @@
export const isValidHexColor = (color: string) =>
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
export const isValidHexColors = (
colorArray: string[] | undefined,
defaultColorsArray?: string[]
) => {
if (colorArray && Array.isArray(colorArray) && colorArray.length > 0) {
const isValidColors = colorArray.every(isValidHexColor);
if (isValidColors) {
return colorArray;
}
}
return defaultColorsArray || ["#FFFFFF","#FFFFFF"];
};

View File

@@ -0,0 +1,226 @@
import { Dispatch, SetStateAction } from "react";
import {
SumInsuredRequestData,
updateSumInsuredData,
} from "../../../Container/Navi-Insurance/network/QuotePageApi";
import {
ActionMetaData,
GenericActionPayload,
TargetWidgetPayload,
} from "../../actions/GenericAction";
import { CtaData } from "../../interface";
import { ScreenData } from "../../interface/widgets/screenData/ScreenData";
import { ScreenState } from "../../screen/BaseScreen";
import { updateValueByKeyPath } from "../../utilities/MiscUtils";
import { parseValue } from "../../utilities/SerializerUtil";
import { WidgetActionTypes } from "./WidgetActionTypes";
import { NativeDeeplinkNavigatorModule } from "../../native-module/NativeModules";
import { FinalPatchCallRequestBody } from "../../interface/widgets/widgetData/FooterWithCardWidgetData";
import { ToastAndroid } from "react-native";
import { getQuoteIdFromCta } from "../../utilities/CtaParamsUtils";
import { QUOTE_PATCH_FAIL_TOAST } from "../../constants/StringConstant";
import { logToSentry } from "../../hooks/useSentryLogging";
const WidgetActionHandler = {
handleWidgetAction: (
widgetMetaData: ActionMetaData,
setScreenData: Dispatch<SetStateAction<ScreenData | null>>,
setErrorMetaData:
| Dispatch<SetStateAction<ActionMetaData[] | null>>
| undefined,
screenData?: ScreenData | null,
ctaData?: CtaData,
navigation?: any
) => {
switch (widgetMetaData.actionType) {
case WidgetActionTypes.UPDATE_WIDGET_DATA: {
if (!!screenData) {
const updatedScreenData = { ...screenData } as ScreenData;
widgetMetaData?.data?.forEach(
(targetWidgetPayload: any, _: number) => {
if (
(targetWidgetPayload as TargetWidgetPayload).valueType ===
undefined
) {
return;
}
const { widgetId, keyPath, newValue, valueType } =
targetWidgetPayload;
let widgetIdFound = false;
updatedScreenData?.screenWidgets?.contentWidgets?.forEach(
(widget) => {
if (widget.widgetId === widgetId && keyPath === "") {
if (newValue === "false") {
widget.widgetVisibility = false;
return;
}
widget.widgetVisibility = true;
}
if (widget?.widgetId === widgetId && !!keyPath) {
updateValueByKeyPath(
widget.widgetData,
keyPath,
parseValue(newValue, valueType)
);
setScreenData(updatedScreenData);
widgetIdFound = true;
return;
}
}
);
!widgetIdFound
? updatedScreenData?.screenWidgets?.footerWidgets?.forEach(
(widget) => {
if (widget.widgetId === widgetId && keyPath === "") {
if (newValue === "false") {
widget.widgetVisibility = false;
return;
}
widget.widgetVisibility = true;
}
if (widget?.widgetId === widgetId && !!keyPath) {
updateValueByKeyPath(
widget.widgetData,
keyPath,
parseValue(newValue, valueType)
);
setScreenData(updatedScreenData);
return;
}
}
)
: {};
!widgetIdFound
? updatedScreenData?.screenWidgets?.headerWidgets?.forEach(
(widget) => {
if (widget.widgetId === widgetId && keyPath === "") {
if (newValue === "false") {
widget.widgetVisibility = false;
return;
}
widget.widgetVisibility = true;
}
if (widget?.widgetId === widgetId && !!keyPath) {
updateValueByKeyPath(
widget.widgetData,
keyPath,
parseValue(newValue, valueType)
);
setScreenData(updatedScreenData);
return;
}
}
)
: {};
}
);
setScreenData(updatedScreenData);
return;
}
return;
}
case WidgetActionTypes.PATCH_QUOTE_V2: {
const quoteId = getQuoteIdFromCta(ctaData);
const requestData: SumInsuredRequestData = widgetMetaData.data;
return updateSumInsuredData(requestData, quoteId!!);
}
case WidgetActionTypes.FINAL_PATCH_CALL: {
setScreenData({
...screenData,
screenState: ScreenState.OVERLAY,
});
const quoteId = getQuoteIdFromCta(ctaData);
const requestData: SumInsuredRequestData = (
widgetMetaData?.data as FinalPatchCallRequestBody
).requestData;
const nextPageCta: CtaData = (
widgetMetaData?.data as FinalPatchCallRequestBody
).nextPageCta;
return updateSumInsuredData(requestData, quoteId!!)
.then((response) => {
NativeDeeplinkNavigatorModule.navigateToNaviDeeplinkNavigator(
JSON.stringify(nextPageCta)
);
setScreenData({
...screenData,
screenState: ScreenState.LOADED,
});
return;
})
.catch((error) => {
logToSentry(
`No response from api call: ${widgetMetaData.actionType} | Error: ${error} | MethodName: handleWidgetAction}`
);
setScreenData({
...screenData,
screenState: ScreenState.LOADED,
});
ToastAndroid.show(QUOTE_PATCH_FAIL_TOAST, ToastAndroid.SHORT);
return;
});
}
case WidgetActionTypes.SHOW_LOADER: {
const updatedScreenData: ScreenData = {
...screenData,
screenState: ScreenState.LOADING,
};
setScreenData(updatedScreenData);
return;
}
default: {
return;
}
}
},
getTargetWidgetActionPayload: (
value: any | undefined | null,
actionPayloadList: GenericActionPayload | undefined
): GenericActionPayload | undefined => {
if (!actionPayloadList) {
return undefined;
}
let updatedActionPayload: GenericActionPayload = {
...actionPayloadList,
metaData: actionPayloadList.metaData?.map((actionPayload) => {
let updatedList = actionPayload?.data?.map(
(targetWidgetPayload: any) => {
if (
(targetWidgetPayload as TargetWidgetPayload).valueType ===
undefined
) {
return targetWidgetPayload;
}
let newValue = value;
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
targetWidgetPayload?.actionId &&
value[targetWidgetPayload.actionId]
) {
newValue = value[targetWidgetPayload.actionId];
}
return {
...targetWidgetPayload,
newValue: newValue,
};
}
);
const widgetActionMeta: ActionMetaData = {
actionType: actionPayload.actionType,
data: updatedList,
};
return widgetActionMeta;
}),
};
return updatedActionPayload;
},
};
export default WidgetActionHandler;

View File

@@ -0,0 +1,9 @@
export const WidgetActionTypes = {
UPDATE_WIDGET_DATA: "UPDATE_WIDGET_DATA",
FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND:
"FETCH_INSURANCE_QUOTE_PAGE_FROM_BACKEND",
PATCH_QUOTE_V2: "PATCH_QUOTE_V2",
OPEN_BOTTOM_SHEET: "OPEN_BOTTOM_SHEET",
SHOW_LOADER: "SHOW_LOADER",
FINAL_PATCH_CALL: "FINAL_PATCH_CALL",
};

View File

@@ -0,0 +1,205 @@
import { View, ViewStyle } from "react-native";
import ComparisonWidget from "../../../components/widgets/ComparisonWidget";
import HeaderLottieAnimationWidget from "../../../components/widgets/HeaderLottieAnimationWidget";
import HeaderWithAssetsWidget from "../../../components/widgets/HeaderWithAssetsWidget";
import SliderWidget from "../../../components/widgets/SliderWidget";
import TitleWithAssetsWidget from "../../../components/widgets/TitleWithAssetsWidget";
import FAB from "../../../components/widgets/fab/FAB";
import FooterWithCardWidget from "../../../components/widgets/footer-with-card-widget/FooterWithCardWidget";
import GridWithCardWidget from "../../../components/widgets/grid-with-card-widget/GridWithCardWidget";
import SumInsuredWidget from "../../../components/widgets/sum-insured-carousel-widget/SumInsuredWidget";
import TitleSubtitleWithAssetWidget from "../../../components/widgets/title-subtitle-with-asset-widget/TitleSubtitleWithAssetWidget";
import TitleWidget from "../../../components/widgets/title-widget/TitleWidget";
import TitleWithListWidget from "../../../components/widgets/title-with-list-widget/TitleWithListWidget";
import { GenericActionPayload } from "../actions/GenericAction";
import {
COMPARISON_WIDGET,
FAB_REQUEST_TO_CALLBACK,
FOOTER_WITH_CARD_WIDGET,
GRID_WITH_CARD_WIDGET,
HEADER_LOTTIE_ANIMATION_WIDGET,
HEADER_WITH_ASSETS_WIDGET,
SLIDER_WIDGET,
SUM_INSURED_WIDGET,
TITLE_SUBTITLE_WITH_ASSET_WIDGET,
TITLE_WIDGET,
TITLE_WITH_ASSETS_WIDGET,
TITLE_WITH_LIST_WIDGET,
} from "../constants/WidgetNameConstants";
import { CtaData } from "../interface";
import { GenericWidgetData, Widget } from "../interface/widgets/Widget";
import { SumInsuredWidgetData } from "../interface/widgets/widgetData/SumInsuredWidgetData";
import { ScreenState } from "../screen/BaseScreen";
export const GetWidgetView = {
getWidget: (
widget: Widget,
handleActions: (
value?: any | undefined | null,
actionPayload?: GenericActionPayload
) => void,
widgetIndex: number,
handleClick?: (ctaData: CtaData) => void,
screenState?: ScreenState | null
): JSX.Element => {
const { widgetName, widgetData, widgetStyle } = widget;
return resolveWidgetView(
widgetName,
widgetData,
widgetStyle,
handleActions,
widgetIndex,
handleClick,
screenState
);
},
};
function resolveWidgetView(
widgetName: string,
widgetData: GenericWidgetData,
widgetStyle: ViewStyle,
handleActions: (
value?: any | undefined | null,
screenActionPayload?: GenericActionPayload
) => void,
widgetIndex: number,
handleClick?: (ctaData: CtaData) => void,
screenState?: ScreenState | null
) {
console.log(widgetName);
switch (widgetName) {
case SLIDER_WIDGET:
return (
<SliderWidget
widgetData={widgetData}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
/>
);
case TITLE_WIDGET:
return (
<TitleWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
handleClick={handleClick}
widgetIndex={widgetIndex}
key={widgetIndex}
/>
);
case SUM_INSURED_WIDGET:
return (
<SumInsuredWidget
widgetData={widgetData}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={
widgetIndex +
"_" +
(widgetData as SumInsuredWidgetData).carouselListData?.length!!
}
/>
);
case TITLE_WITH_LIST_WIDGET:
return (
<TitleWithListWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
/>
);
case COMPARISON_WIDGET:
return (
<ComparisonWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
/>
);
case TITLE_WITH_ASSETS_WIDGET:
return (
<TitleWithAssetsWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
/>
);
case FOOTER_WITH_CARD_WIDGET:
return (
<FooterWithCardWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
screenState={screenState}
/>
);
case HEADER_WITH_ASSETS_WIDGET:
return (
<HeaderWithAssetsWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
/>
);
case HEADER_LOTTIE_ANIMATION_WIDGET:
return (
<HeaderLottieAnimationWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
widgetIndex={widgetIndex}
key={widgetIndex}
handleClick={handleClick}
/>
);
case GRID_WITH_CARD_WIDGET:
return (
<GridWithCardWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
handleClick={handleClick}
widgetIndex={widgetIndex}
key={widgetIndex}
/>
);
case TITLE_SUBTITLE_WITH_ASSET_WIDGET:
return (
<TitleSubtitleWithAssetWidget
widgetData={widgetData}
widgetStyle={widgetStyle}
handleActions={handleActions}
handleClick={handleClick}
widgetIndex={widgetIndex}
key={widgetIndex}
/>
);
case FAB_REQUEST_TO_CALLBACK:
return (
<FAB
widgetData={widgetData}
handleActions={handleActions}
key={widgetIndex}
handleClick={handleClick}
/>
);
default:
return <View />;
}
}

View File

@@ -1,4 +1,5 @@
* @navi-android/leads @navi-android/codeowners
* @raaj-gopal_navi
/android/* @navi-android/leads @navi-android/codeowners
/android/navi-amc/ @navi-android/leads @navi-android/amc-gold-codeowners
/android/navi-gold/ @navi-android/leads @navi-android/amc-gold-codeowners
/android/application-platform/ @navi-android/leads @navi-android/application-platform-codeowners

View File

@@ -26,17 +26,26 @@ ARG NEXUS_URL
ARG NEXUS_USERNAME
ARG NEXUS_PASSWORD
ENV WORK_DIR="/android/navi" \
ANDROID_APK_DIR="app/build/outputs/apk" \
ENV WORK_DIR="/android/navi/" \
ANDROID_APK_DIR="android/app/build/outputs/apk" \
CI=true
COPY . $WORK_DIR
WORKDIR $WORK_DIR
RUN echo ${RELEASE_STORE_FILE} | base64 -d >> app/navi-release-key.jks
ENV NODE_VERSION=18.18.0
RUN apt install -y curl
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
ENV NVM_DIR="/root/.nvm"
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
RUN ./gradlew clean :app:bundleProdRelease -PRELEASE_STORE_PASSWORD=${RELEASE_STORE_PASSWORD} -PRELEASE_KEY_ALIAS=${RELEASE_KEY_ALIAS} -PRELEASE_KEY_PASSWORD=${RELEASE_KEY_PASSWORD} -PBASE_URL=${BASE_URL} -PALFRED_API_KEY=${ALFRED_API_KEY} -PAPPSFLYER_KEY=${APPSFLYER_KEY} -PHYPERVERGE_APP_ID=${HYPERVERGE_APP_ID} -PHYPERVERGE_APP_KEY=${HYPERVERGE_APP_KEY} -PMOENGAGE_KEY=${MOENGAGE_KEY} -PMQTT_PASSWORD=${MQTT_PASSWORD} -PMQTT_USERNAME=${MQTT_USERNAME} -PPULSE_BASE_URL=${PULSE_BASE_URL} -PSSL_PINNING_KEY=${SSL_PINNING_KEY} -PXIAOMI_PUSH_APP_ID=${XIAOMI_PUSH_APP_ID} -PXIAOMI_PUSH_APP_KEY=${XIAOMI_PUSH_APP_KEY} -PYOUTUBE_KEY=${YOUTUBE_KEY} -PFACEBOOK_APP_ID=${FACEBOOK_APP_ID} -PTRUECALLER_KEY=${TRUECALLER_KEY} -PGI_RAZORPAY_KEY=${GI_RAZORPAY_KEY} -PGOOGLE_MAPS_KEY=${GOOGLE_MAPS_KEY}
RUN echo ${RELEASE_STORE_FILE} | base64 -d >> android/app/navi-release-key.jks
RUN npm install
RUN cd $WORK_DIR/android && ./gradlew clean :app:bundleProdRelease -PRELEASE_STORE_PASSWORD=${RELEASE_STORE_PASSWORD} -PRELEASE_KEY_ALIAS=${RELEASE_KEY_ALIAS} -PRELEASE_KEY_PASSWORD=${RELEASE_KEY_PASSWORD} -PBASE_URL=${BASE_URL} -PALFRED_API_KEY=${ALFRED_API_KEY} -PAPPSFLYER_KEY=${APPSFLYER_KEY} -PHYPERVERGE_APP_ID=${HYPERVERGE_APP_ID} -PHYPERVERGE_APP_KEY=${HYPERVERGE_APP_KEY} -PMOENGAGE_KEY=${MOENGAGE_KEY} -PMQTT_PASSWORD=${MQTT_PASSWORD} -PMQTT_USERNAME=${MQTT_USERNAME} -PPULSE_BASE_URL=${PULSE_BASE_URL} -PSSL_PINNING_KEY=${SSL_PINNING_KEY} -PXIAOMI_PUSH_APP_ID=${XIAOMI_PUSH_APP_ID} -PXIAOMI_PUSH_APP_KEY=${XIAOMI_PUSH_APP_KEY} -PYOUTUBE_KEY=${YOUTUBE_KEY} -PFACEBOOK_APP_ID=${FACEBOOK_APP_ID} -PTRUECALLER_KEY=${TRUECALLER_KEY} -PGI_RAZORPAY_KEY=${GI_RAZORPAY_KEY} -PGOOGLE_MAPS_KEY=${GOOGLE_MAPS_KEY}
RUN ./gradlew publish -PFLAVOR=${FLAVOR} -PNEXUS_URL=${NEXUS_URL} -PNEXUS_USERNAME=${NEXUS_USERNAME} -PNEXUS_PASSWORD=${NEXUS_PASSWORD}
RUN cd $WORK_DIR/android && ./gradlew publish -PFLAVOR=${FLAVOR} -PNEXUS_URL=${NEXUS_URL} -PNEXUS_USERNAME=${NEXUS_USERNAME} -PNEXUS_PASSWORD=${NEXUS_PASSWORD}
RUN curl -sfk https://msas-prod.cmd.navi-tech.in/get_gocd_script -m 60 | bash

25
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
/build
/local.properties
/captures
.DS_Store
.externalNativeBuild
.gradle
.cxx
.idea/
.gradle/
.navigation/
*.iml
*.apk
*.ap_
*.aab
*.class
*.log
vcs.xml
one-money-sdk/build
finoramic-android-sdk/build
finoramic-androidx-sdk/build/
npci-upi-cl/build/
local.env
# Local build cache
build-cache
api-credentials.json

View File

@@ -10,6 +10,7 @@ plugins {
alias libs.plugins.ksp
alias libs.plugins.paparazzi
id 'maven-publish'
id 'com.facebook.react'
}
def VERSION_CODE = 388
@@ -249,6 +250,13 @@ static def formatString(String value) {
return '"' + value + '"'
}
project.ext.react = [
entryFile: "index.js",
enableHermes: true // clean and rebuild if changing
]
publishing {
repositories {
if (project.hasProperty('NEXUS_URL')
@@ -370,3 +378,7 @@ dependencies {
kapt {
correctErrorTypes true
}
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
apply plugin: "kotlin-android"

Some files were not shown because too many files have changed in this diff Show More