CRUD operation using SAP UI5 and Google Firestore | Firebase

Introduction

Welcome to the comprehensive guide on performing CRUD (Create, Read, Update, Delete) operations using SAP UI5 and Google Firestore! If you’re looking to build a robust web application with real-time data management capabilities, you’re in the right place.

In this tutorial, we’ll walk you through the process of integrating SAP UI5, a powerful JavaScript framework for building web applications, with Google Firestore, a flexible and scalable NoSQL database. By combining these technologies, you’ll be able to create a dynamic and interactive application that allows users to perform CRUD operations seamlessly.

Throughout this tutorial, we’ll cover all aspects of the CRUD operations using SAP UI5 and Google Firestore. We’ll guide you through setting up a project in SAP UI5 and configuring the necessary dependencies. You’ll also learn how to create a collection in Google Firestore to store your application’s data.

We’ll then delve into the implementation of each CRUD operation. You’ll discover how to create new records, retrieve data, update existing records, and delete entries from the Firestore database using SAP UI5. We’ll provide you with code examples, best practices, and tips to ensure a smooth and efficient implementation.

Additionally, we’ll explore real-time data synchronization capabilities offered by Google Firestore. You’ll learn how to leverage Firestore’s powerful listeners to automatically update your SAP UI5 application whenever changes occur in the database. This will enable you to build responsive and real-time applications that provide an exceptional user experience.

By the end of this tutorial, you’ll have a solid understanding of how to perform CRUD operations using SAP UI5 and Google Firestore. You’ll be able to create, read, update, and delete data in your application’s database with ease.

So, whether you’re a seasoned SAP UI5 developer or just starting your journey in web application development, this tutorial is perfect for you. Get ready to unlock the full potential of SAP UI5 and Google Firestore as you master the art of performing CRUD operations in your applications. Let’s get started on this exciting journey of building dynamic and data-driven web applications!

Steps to perform CRUD operation using SAP UI5 and Google Firestore

Sure! Here are the steps to perform CRUD (Create, Read, Update, Delete) operations using SAP UI5 and Google Firestore:

1. Set up your SAP UI5 project: Create a new SAP UI5 project or use an existing one. Set up the necessary project structure and dependencies.

2. Set up Google Firestore: Go to the Google Cloud Console (https://console.cloud.google.com/) and create a new project or use an existing one. Enable Firestore for your project and set up the necessary Firestore collections and documents to store your application’s data.

3. Connect SAP UI5 with Google Firestore: In your SAP UI5 project, create a new JavaScript file (e.g., “firestore.js”) to handle the connection between SAP UI5 and Google Firestore. Import the necessary Firestore libraries and initialize the Firestore client with your project’s configuration.

4. Implement Create operation: Create a user interface in your SAP UI5 app where users can input data for creating new records. In the “firestore.js” file, implement the logic to add new documents to the Firestore collection using the Firestore client API.

5. Implement Read operation: Create a user interface to display the existing data from Firestore. In the “firestore.js” file, retrieve the data from the Firestore collection using the Firestore client API and bind it to the SAP UI5 controls for display.

6. Implement Update operation: Create a user interface to allow users to edit existing records. Implement the logic to update the Firestore documents when the user makes changes using the Firestore client API.

7. Implement Delete operation: Add functionality to delete records from Firestore. Create a user interface to display the existing records and allow users to delete specific entries. Implement the logic to remove documents from the Firestore collection using the Firestore client API.

8. Update Rules in Firebase: Rules in Firebase are used to restrict wrong users from performing any CRUD operation over data base. You can read more about it here. As of now, we have removed all the restrictions for testing purposes.

Rules update in Firebase

9. Test and refine: Thoroughly test your CRUD operations to ensure they are working correctly. Verify that new records can be created, existing records can be retrieved, updated, and deleted. Handle any error scenarios gracefully and provide appropriate feedback to the user.

By following these steps, you’ll be able to perform CRUD operations using SAP UI5 and Google Firestore. Remember to consult the documentation for SAP UI5 and Google Firestore for more detailed instructions and best practices.

UI5 Code to Perform CRUD operation using Google Firestore

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>firebaseApp</title>
        <script id="sap-ui-bootstrap"
            src="resources/sap-ui-core.js"
            data-sap-ui-theme="sap_fiori_3"
            data-sap-ui-resourceroots='{"firebaseApp.firebaseApp": "./"}'
            data-sap-ui-compatVersion="edge"
            data-sap-ui-oninit="module:sap/ui/core/ComponentSupport"
            data-sap-ui-async="true"
            data-sap-ui-frameOptions="trusted">
        </script>
         <!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-app.js"></script>

  <!-- If you enabled Analytics in your project, add the Firebase SDK for Analytics -->
  <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-analytics.js"></script>

  <!-- Add Firebase products that you want to use -->
  <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-firestore.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-database.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.23.0/firebase-storage.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.8.0/firebase-functions.js"></script>
        
    </head>
    <body class="sapUiBody">
        <div data-sap-ui-component data-name="firebaseApp.firebaseApp" data-id="container" data-settings='{"id" : "firebaseApp"}'></div>
    </body>
</html>

 

Firebase.js

sap.ui.define([
    "sap/ui/model/json/JSONModel",
], function (JSONModel) {
    "use strict";
    return {
        // Firebase-config retrieved from the Firebase-console
        initializeFirebase: function () {
            // Replace with your config here
            const firebaseConfig = {
                apiKey: "AIzaSyBGCi3pHJZqMmpJEeNjnoII8Ic_BL2v8vU",
                authDomain: "myprojectideas-c2512.firebaseapp.com",
                projectId: "myprojectideas-c2512",
                storageBucket: "myprojectideas-c2512.appspot.com",
                messagingSenderId: "121838639924",
                appId: "1:121838639924:web:39794cab14d51551172361"
            };
            // Initialize Firebase with the Firebase-config
            firebase.initializeApp(firebaseConfig);

            // Create a Firestore reference
            const firestore = firebase.firestore();

            // Create a Authentication reference
            const fireAuth = firebase.auth();

            // Get Firebase Instance
            const oFirestore = firebase.firestore;

            // Create a Fire Storage reference
            const fireStorage = firebase.storage();

            // Create a Fire Functions reference
            const fireFunctions = firebase.app().functions('asia-east2');

            // Firebase services object
            const oFirebase = {
                firestore: firestore,
                fireAuth: fireAuth,
                oFirestore: oFirestore,
                fireStorage: fireStorage,
                fireFunctions: fireFunctions
            };

            // Create a Firebase model out of the oFirebase service object which contains all required Firebase services
            var fbModel = new JSONModel(oFirebase);

            // Return the Firebase Model
            return fbModel;
        }
    };
});

 

model.js

sap.ui.define([
    "sap/ui/model/json/JSONModel",
    "sap/ui/Device"
], function (JSONModel, Device) {
    "use strict";

    return {

        createDeviceModel: function () {
            var oModel = new JSONModel(Device);
            oModel.setDefaultBindingMode("OneWay");
            return oModel;
        },

        //app Congiguration model
        createAppConfigModel: function () {
            var appData = {
                "otpSent": false
            };
            var oModel = new JSONModel(appData);
            return oModel;
        }

    };
});

 

manifest.json

{
    "_version": "1.12.0",
    "sap.app": {
        "id": "firebaseApp.firebaseApp",
        "type": "application",
        "i18n": "i18n/i18n.properties",
        "applicationVersion": {
            "version": "1.0.0"
        },
        "title": "{{appTitle}}",
        "description": "{{appDescription}}",
        "sourceTemplate": {
            "id": "ui5template.basicSAPUI5ApplicationProject",
            "version": "1.40.12"
        }
    },
    "sap.ui": {
        "technology": "UI5",
        "icons": {
            "icon": "",
            "favIcon": "",
            "phone": "",
            "phone@2": "",
            "tablet": "",
            "tablet@2": ""
        },
        "deviceTypes": {
            "desktop": true,
            "tablet": true,
            "phone": true
        }
    },
    "sap.ui5": {
        "flexEnabled": false,
        "rootView": {
            "viewName": "firebaseApp.firebaseApp.view.Login",
            "type": "XML",
            "async": true,
            "id": "Login"
        },
        "dependencies": {
            "minUI5Version": "1.65.6",
            "libs": {
                "sap.ui.layout": {},
                "sap.ui.core": {},
                "sap.m": {}
            }
        },
        "contentDensities": {
            "compact": true,
            "cozy": true
        },
        "models": {
            "i18n": {
                "type": "sap.ui.model.resource.ResourceModel",
                "settings": {
                    "bundleName": "firebaseApp.firebaseApp.i18n.i18n"
                }
            }
        },
        "resources": {
            "css": [{
                "uri": "css/style.css"
            }]
        },
        "routing": {
            "config": {
                "routerClass": "sap.m.routing.Router",
                "viewType": "XML",
                "async": true,
                "viewPath": "firebaseApp.firebaseApp.view",
                "controlAggregation": "pages",
                "controlId": "app",
                "clearControlAggregation": false
            },
            "routes": [{
                "pattern": "",
                "name": "",
                "target": "Login"
            }, {
                "pattern": "Main",
                "name": "Main",
                "target": "Main"
            }],
            "targets": {
                "Login": {
                    "viewType": "XML",
                    "transition": "slide",
                    "clearControlAggregation": false,
                    "viewId": "Login",
                    "viewName": "Login"
                },
                "Main": {
                    "viewType": "XML",
                    "transition": "slide",
                    "clearControlAggregation": false,
                    "viewId": "Main",
                    "viewName": "Main"
                }
            }
        }
    }
}

 

component.js

sap.ui.define([
    "sap/ui/core/UIComponent",
    "sap/ui/Device",
    "firebaseApp/firebaseApp/model/models",
    "firebaseApp/firebaseApp/js/Firebase"
], function (UIComponent, Device, models, Firebase) {
    "use strict";

    return UIComponent.extend("firebaseApp.firebaseApp.Component", {

        metadata: {
            manifest: "json"
        },

        /**
         * The component is initialized by UI5 automatically during the startup of the app and calls the init method once.
         * @public
         * @override
         */
        init: function () {
            // call the base component's init function
            UIComponent.prototype.init.apply(this, arguments);

            // enable routing
            this.getRouter().initialize();

            // set the device model
            this.setModel(models.createDeviceModel(), "device");

            // set the app Config model
            this.setModel(models.createAppConfigModel(), "AppConfig");

            //set Firebase Model
            this.setModel(Firebase.initializeFirebase(), "fbModel");
        }
    });
});

 

login.view.xml

<mvc:View controllerName="firebaseApp.firebaseApp.controller.Login" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m"
    xmlns:html="http://www.w3.org/1999/xhtml" xmlns:core="sap.ui.core">
    <Shell id="shell">
        <App id="app">
            <pages>
                <Page id="page" title="OTP Login Integration in SAP UI5 using Google Firebase">
                    <content>
                        <VBox height="100%" alignItems="Center">
                            <HBox height="100%" alignItems="Center">
                                <Image height="70px" class="sapUiLargeMarginEnd"
                                    src="https://www.loc-online.co.uk/berkshire-loc/wp-content/uploads/sites/23/2020/06/Screenshot-2020-05-28-at-16.35.37-266x300.png"/>
                                <IconTabBar id="idIconTabBarNoIcons" expanded="{device>/isNoPhone}" selectedKey="mail" class="sapUiResponsiveContentPadding">
                                    <items>
                                        <IconTabFilter text="Mobile Login" key="mobile">
                                            <VBox id="idInitialDetails">
                                                <Label design="Bold" text="Enter Registered Mobile number" required="true"/>
                                                <HBox>
                                                    <ComboBox id="idcc" selectedKey="65" width="90px" class="sapUiTinyMarginEnd">
                                                        <core:Item key="65" text="+65"/>
                                                        <core:Item key="60" text="+60"/>
                                                        <core:Item key="62" text="+62"/>
                                                        <core:Item key="91" text="+91"/>
                                                    </ComboBox>
                                                    <Input width="200px" id="idMob" required="true" type="Number" placeholder="0000000" submit="ongetOTP"/>
                                                </HBox>
                                                <HBox visible="{= ${AppConfig>/otpSent} === true ? false : true}">
                                                    <Button width="145px" class="sapUiTinyMarginEnd" text="Get OTP" press="ongetOTP"/>
                                                    <Button width="145px" text="Reset" press="onReset"/>
                                                </HBox>
                                                <Label visible="{AppConfig>/otpSent}" design="Bold" text="OTP" required="true"/>
                                                <Input visible="{AppConfig>/otpSent}" width="300px" id="idOTP" required="true" submit="onASignin"/>
                                                <HBox visible="{AppConfig>/otpSent}">
                                                    <Button width="145px" class="sapUiTinyMarginEnd" text="Sign In" press="onASignin"/>
                                                    <Button width="145px" text="Resend OTP" press="ongetOTP"/>
                                                </HBox>
                                            </VBox>
                                        </IconTabFilter>
                                    </items>
                                </IconTabBar>
                                <!--Code for Recaptcha-->
                                <core:HTML content='&lt;div id=&quot;sign-in-button&quot;&gt;&lt;/div&gt;'></core:HTML>
                            </HBox>
                        </VBox>
                    </content>
                </Page>
            </pages>
        </App>
    </Shell>
</mvc:View>

 

login.controller.js

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/m/MessageBox"
], function (Controller, MessageBox) {
    "use strict";

    return Controller.extend("firebaseApp.firebaseApp.controller.Login", {
        onInit: function () {
            this.oRouter = sap.ui.core.UIComponent.getRouterFor(this);
        },

        onAfterRendering: async function () {
            sap.ui.core.BusyIndicator.show();
            await this.onGetRecaptcha();
            sap.ui.core.BusyIndicator.hide();
        },

        onGetRecaptcha: function (oEvent) {
            var that = this;
            sap.ui.core.BusyIndicator.show();
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
                'size': 'invisible',
                'callback': (response) => {
                    // reCAPTCHA solved, allow signInWithPhoneNumber.
                    sap.ui.core.BusyIndicator.hide();
                }
            });
        },

        ongetOTP: function () {
            sap.ui.core.BusyIndicator.show();
            var appVerifier = window.recaptchaVerifier;
            var that = this;
            var cc = this.byId("idcc")._getSelectedItemText();
            var mob = this.byId("idMob").getValue();
            mob = cc + mob;
            this.phone = mob;
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            firebase.auth().signInWithPhoneNumber(mob, appVerifier)
                .then((confirmationResult) => {
                    sap.ui.core.BusyIndicator.hide();
                    // SMS sent. Prompt user to type the code from the message, then sign the
                    // user in with confirmationResult.confirm(code).
                    window.confirmationResult = confirmationResult;
                    that.getOwnerComponent().getModel("AppConfig").setProperty("/otpSent", true);
                }).catch((error) => {
                    sap.ui.core.BusyIndicator.hide();
                    // Error; SMS not sent
                    var errorMessage = error.message;
                    that.getOwnerComponent().getModel("AppConfig").setProperty("/otpSent", false);
                    MessageBox.error(errorMessage);
                });
        },

        onASignin: async function (oEvent) {
            sap.ui.core.BusyIndicator.show();
            var that = this;
            var otp = this.byId("idOTP").getValue();
            var errorMessage = "";
            // Create a Fireauth Auth reference
            var oModel = that.getView().getModel("fbModel").getData();
            localStorage.setItem("oModelFireAuth", JSON.stringify(oModel.fireAuth));
            var fireAuth = oModel.fireAuth;
            var firestoreData = oModel.firestore;
            var fireFunctions = oModel.fireFunctions;
            //get Token for the call
            window.confirmationResult.confirm(otp).then(async(result) => {
                await fireAuth.currentUser.getIdToken( /* forceRefresh */ true).then(async function (idToken) {
                    MessageBox.success("You are logged in via OTP!");
                    that.oRouter.navTo("Main");
                    sap.ui.core.BusyIndicator.hide();
                }).catch(function (error) {
                    sap.ui.core.BusyIndicator.hide();
                    that.onReset();
                    MessageBox.error("User is not registered, kindly contact Admin.");
                    fireAuth.signOut().then(function (success) {
                        // MessageBox.error("You are logged out, refresh and try again!");
                    }).catch(function (error) {
                        MessageBox.error(error.error.Error);
                    });
                });
            }).catch((error) => {
                // User couldn't sign in (bad verification code?)
                sap.ui.core.BusyIndicator.hide();
                errorMessage = error.message;
                MessageBox.error(errorMessage);
                fireAuth.signOut().then(function (success) {
                    // jQuery.sap.storage.put(that.userID, null);
                }).catch(function (error) {});
            });
        },

        onReset: function (oEvent) {
            this.byId("idOTP").setValue("");
            this.byId("idMob").setValue("");
            this.getOwnerComponent().getModel("AppConfig").setProperty("/otpSent", false);
        }
    });
});

 

main.view.xml

<mvc:View xmlns:core="sap.ui.core" xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form"
    controllerName="firebaseApp.firebaseApp.controller.Main" xmlns:html="http://www.w3.org/1999/xhtml">
    <App>
        <pages>
            <Page title="CRUD operation using SAP UI5 and Google Firestore">
                <content>
                    <VBox class="sapUiSmallMargin">
                        <f:SimpleForm id="SimpleFormOrg" editable="true" layout="ResponsiveGridLayout" title="Organizations" labelSpanXL="3" labelSpanL="3"
                            labelSpanM="3" labelSpanS="12" adjustLabelSpan="false" emptySpanXL="4" emptySpanL="4" emptySpanM="4" emptySpanS="0" columnsXL="1"
                            columnsL="1" columnsM="1" singleContainerFullSize="false">
                            <f:content>
                                <Label text="Name of Organization"/>
                                <Input id="idOrgName"/>
                                <Button text="Add" type="Accept" press="onAddOrg"/>
                            </f:content>
                        </f:SimpleForm>
                        <Table id="idOrgTable" inset="false" items="{/orgs}" sticky="ColumnHeaders,HeaderToolbar" class="sapFDynamicPageAlignContent" width="auto">
                            <columns>
                                <Column>
                                    <Text text="ID"/>
                                </Column>
                                <Column>
                                    <Text text="Name of Organization"/>
                                </Column>
                                <Column vAlign="Right">
                                    <Text text="New Name"/>
                                </Column>
                                <Column vAlign="Right">
                                    <Text text="Perform Operations"/>
                                </Column>
                            </columns>
                            <items>
                                <ColumnListItem>
                                    <cells>
                                        <Text text="{ID}"/>
                                        <Text text="{name}"/>
                                        <Input value="{newName}"/>
                                        <HBox alignItems="Center" justifyContent="Start">
                                            <Button icon="sap-icon://edit" press="onUpdate"/>
                                            <Button icon="sap-icon://delete" press="onDelete"/>
                                        </HBox>
                                    </cells>
                                </ColumnListItem>
                            </items>
                        </Table>
                    </VBox>
                </content>
            </Page>
        </pages>
    </App>
</mvc:View>

 

main.controller.js

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/m/MessageBox"
], function (Controller, MessageBox) {
    "use strict";

    return Controller.extend("firebaseApp.firebaseApp.controller.Main", {

        /**
         * Called when a controller is instantiated and its View controls (if available) are already created.
         * Can be used to modify the View before it is displayed, to bind event handlers and do other one-time initialization.
         * @memberOf firebaseApp.firebaseApp.view.Main
         */
        onInit: function (oEvent) {
            this.oRouter = sap.ui.core.UIComponent.getRouterFor(this);
        },

        onAfterRendering: async function () {
            this.onSelectOrg();
        },

        onSelectOrg: function () {
            var that = this;
            var oItems = {};
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            var collRefUsers = firestoreData.collection("Organization");
            collRefUsers.onSnapshot(collection => {
                var aItems = collection.docs.map(function (docUser) {
                    var finalData = docUser.data();
                    finalData.ID = docUser.id;
                    return finalData;
                });
                oItems.orgs = aItems;
                // Create and set the created object to the the UserModel
                var oModel = new sap.ui.model.json.JSONModel(oItems);
                this.getView().byId("idOrgTable").setModel(oModel);
                sap.ui.core.BusyIndicator.hide();
            }, error => {
                sap.ui.core.BusyIndicator.hide();
                // MessageBox.error("Missing or insufficient permissions!");
            });

        },

        onAddOrg: function () {
            var that = this;
            var org = this.byId("idOrgName").getValue();
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            var collRefUsers = firestoreData.collection("Organization");
            if (org) {
                collRefUsers.doc(org).set({
                    name: org
                }).then(function (success) {
                    sap.m.MessageBox.success("Organization Added Successfully!");
                    that.byId("idOrgName").setValue("");
                }).catch(function (error) {
                    // Handle Errors here.
                    var errorMessage = error.message;
                    MessageBox.error(errorMessage);
                });
            } else {
                MessageBox.error("Enter an Organization Name to Add!");
            }
        },

        onDelete: function (oEvent) {
            var that = this;
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            var collRefUsers = firestoreData.collection("Organization");
            var selected = oEvent.getSource().getBindingContext().getObject().ID;
            sap.m.MessageBox.confirm("Do you want to Delete selected Organization?", function (rValue) {
                if (rValue == "OK") {
                    collRefUsers.doc(selected).delete().then(function () {
                        sap.m.MessageBox.success("Organization Deleted!");
                    }).catch(function (error) {
                        // Handle Errors here.
                        var errorMessage = error.message;
                        MessageBox.error(errorMessage);
                    });
                } else {}
            });
        },

        onUpdate: function (oEvent) {
            var that = this;
            var selected = oEvent.getSource().getBindingContext().getObject().name;
            var updated = oEvent.getSource().getBindingContext().getObject().newName;
            // Create a Fireauth Auth reference
            var oModel = this.getView().getModel("fbModel").getData();
            var fireAuth = oModel.fireAuth;
            fireAuth.useDeviceLanguage();
            var firestoreData = oModel.firestore;
            var collRefUsers = firestoreData.collection("Organization");
            sap.m.MessageBox.confirm("Do you want to Update selected Organization?", function (rValue) {
                if (rValue == "OK") {
                    collRefUsers.doc(selected).update({
                        name: updated
                    }).then(function (success) {
                        sap.m.MessageBox.success("Organization Updated Successfully!");
                    }).catch(function (error) {
                        // Handle Errors here.
                        var errorMessage = error.message;
                        MessageBox.error(errorMessage);
                    });
                } else {}
            });
        }

    });

});

Output

CRUD operation using SAP UI5 and Google Firestore

Author

  • Barry Allen

    A Full Stack Developer with 10+ years of experience in different domain including SAP, Blockchain, AI and Web Development.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.