diff --git a/.github/workflows/shiftleft.yml b/.github/workflows/shiftleft.yml new file mode 100644 index 00000000..8dc76b2c --- /dev/null +++ b/.github/workflows/shiftleft.yml @@ -0,0 +1,54 @@ +--- +# This workflow integrates ShiftLeft NG SAST with GitHub +# Visit https://docs.shiftleft.io for help +name: ShiftLeft + +on: + pull_request: + workflow_dispatch: + +jobs: + NextGen-Static-Analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Download ShiftLeft CLI + run: | + curl https://cdn.shiftleft.io/download/sl > ${GITHUB_WORKSPACE}/sl && chmod a+rx ${GITHUB_WORKSPACE}/sl + # ShiftLeft requires Java 1.8 only for java analysis, 11 is recommended otherwise. + - name: Setup Java JDK + uses: actions/setup-java@v1.4.3 + with: + java-version: 11 + - name: Extract branch name + shell: bash + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + id: extract_branch + - name: NextGen Static Analysis + run: ${GITHUB_WORKSPACE}/sl analyze --strict --wait --app mobile-app-android --tag branch=${{ github.head_ref || steps.extract_branch.outputs.branch }} --kotlin . + env: + SHIFTLEFT_ACCESS_TOKEN: ${{ secrets.SHIFTLEFT_ACCESS_TOKEN }} + + + ## Uncomment the following section to enable build rule checking and enforcing. + #Build-Rules: + #runs-on: ubuntu-latest + #needs: NextGen-Static-Analysis + #steps: + #- uses: actions/checkout@v2 + #- name: Download ShiftLeft CLI + # run: | + # curl https://cdn.shiftleft.io/download/sl > ${GITHUB_WORKSPACE}/sl && chmod a+rx ${GITHUB_WORKSPACE}/sl + #- name: Validate Build Rules + # run: | + # ${GITHUB_WORKSPACE}/sl check-analysis --app mobile-app-android \ + # --source 'tag.branch=${{ github.event.pull_request.base.ref }}' \ + # --target "tag.branch=${{ github.head_ref || steps.extract_branch.outputs.branch }}" \ + # --report \ + # --github-pr-number=${{github.event.number}} \ + # --github-pr-user=${{ github.repository_owner }} \ + # --github-pr-repo=${{ github.event.repository.name }} \ + # --github-token=${{ secrets.GITHUB_TOKEN }} + # env: + #SHIFTLEFT_ACCESS_TOKEN: ${{ secrets.SHIFTLEFT_ACCESS_TOKEN }} + \ No newline at end of file diff --git a/.privado/privacy.json b/.privado/privacy.json new file mode 100644 index 00000000..10fd3cdc --- /dev/null +++ b/.privado/privacy.json @@ -0,0 +1,1248 @@ +{ + "repoId": "repoId", + "repoName": "mobile-app-android", + "repoURL": "", + "branchName": "master", + "commitId": "", + "customerId": "cliCustomer", + "scanId": null, + "permissionList": [ + { + "category": "Location Data", + "element": "Location", + "dataElement": "Precise Location", + "sensitive": false, + "sensitivity": "medium", + "score": 1, + "confidence": "high", + "scoringPoints": [], + "occurrences": [ + { + "score": 1, + "confidence": "high", + "scoringPoints": [], + "sample": "android.permission.ACCESS_COARSE_LOCATION", + "fileName": "AndroidManifest.xml", + "language": "app-permissions", + "excerpt": " \n\n \n \n \n\n \n \n \n\n ();\n final List errors = [];\n String firstName;\n String lastName;\n String phoneNumber;\n String address;\n\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 9, + "endIndex": 17 + } + ], + "startLocationLine": 19, + "authorEmail": null + }, + { + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "last_name", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Last Name:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='last_name', name='last_name', value='#{user.last_name}')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Phone Number:\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 33, + "endIndex": 42 + }, + { + "lineNumber": 3, + "startIndex": 51, + "endIndex": 60 + }, + { + "lineNumber": 3, + "startIndex": 77, + "endIndex": 86 + } + ], + "startLocationLine": 42, + "authorEmail": null + } + ] + }, + { + "category": "Personal Identification", + "element": "First Name", + "dataSafetyElement": "Name", + "dataSafetyCategory": "Personal info", + "sensitive": false, + "sensitivity": "low", + "score": 1.5, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "Variable Regex Matched", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 1.5, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "Variable Regex Matched", + "CaseDependant Matched" + ], + "sample": "firstName", + "fileName": "lib/screens/complete_profile/components/complete_profile_form.dart", + "language": "dart", + "excerpt": "class _CompleteProfileFormState extends State {\n final _formKey = GlobalKey();\n final List errors = [];\n String firstName;\n String lastName;\n String phoneNumber;\n String address;\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 9, + "endIndex": 18 + } + ], + "startLocationLine": 18, + "authorEmail": null + }, + { + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "first_name", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right First Name:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='first_name', name='first_name', value='#{user.first_name}')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Last Name:\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 33, + "endIndex": 43 + }, + { + "lineNumber": 3, + "startIndex": 52, + "endIndex": 62 + }, + { + "lineNumber": 3, + "startIndex": 79, + "endIndex": 89 + } + ], + "startLocationLine": 37, + "authorEmail": null + } + ] + }, + { + "category": "Contact Data", + "element": "Email Address", + "dataSafetyElement": "Email address", + "dataSafetyCategory": "Personal info", + "sensitive": false, + "sensitivity": "medium", + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "Email", + "fileName": "apis/views/user/register.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t.panel-body\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right\n\t\t\t\t\t\t\t\t| Email\n\t\t\t\t\t\t\t\tspan.text-red *\n\t\t\t\t\t\t\t\t\t| :\n\t\t\t\t\t\t\t.col-md-6\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 10, + "endIndex": 15 + } + ], + "startLocationLine": 13, + "authorEmail": null + }, + { + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "Email", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t.panel-body\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right\n\t\t\t\t\t\t\t\t| Email\n\t\t\t\t\t\t\t\tspan.text-red\n\t\t\t\t\t\t\t\t\t| :\n\t\t\t\t\t\t\t.col-md-6\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 10, + "endIndex": 15 + } + ], + "startLocationLine": 13, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Variable Regex Matched", + "CaseDependant Matched" + ], + "sample": "email", + "fileName": "lib/constants.dart", + "language": "dart", + "excerpt": "// Form Error\nfinal RegExp emailValidatorRegExp =\n RegExp(r\"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]+\");\nconst String kEmailNullError = \"Please Enter your email\";\nconst String kInvalidEmailError = \"Please Enter Valid Email\";\nconst String kPassNullError = \"Please Enter your password\";\nconst String kShortPassError = \"Password is too short\";\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 1, + "startIndex": 13, + "endIndex": 18 + }, + { + "lineNumber": 3, + "startIndex": 50, + "endIndex": 55 + } + ], + "startLocationLine": 28, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Variable Regex Matched", + "CaseDependant Matched" + ], + "sample": "Email", + "fileName": "lib/constants.dart", + "language": "dart", + "excerpt": "final RegExp emailValidatorRegExp =\n RegExp(r\"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]+\");\nconst String kEmailNullError = \"Please Enter your email\";\nconst String kInvalidEmailError = \"Please Enter Valid Email\";\nconst String kPassNullError = \"Please Enter your password\";\nconst String kShortPassError = \"Password is too short\";\nconst String kMatchPassError = \"Passwords don't match\";\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 2, + "startIndex": 14, + "endIndex": 19 + }, + { + "lineNumber": 3, + "startIndex": 21, + "endIndex": 26 + }, + { + "lineNumber": 3, + "startIndex": 54, + "endIndex": 59 + } + ], + "startLocationLine": 29, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "email", + "fileName": "apis/views/user/login.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\tif message != ''\n\t\t\t\t\t\t\tp.bg-danger.p-d #{message}\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right(for='email') Email:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(name='email', value=\"\", type='text')\n\t\t\t\t\t\t.form-group.clearfix\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 52, + "endIndex": 57 + }, + { + "lineNumber": 5, + "startIndex": 33, + "endIndex": 38 + } + ], + "startLocationLine": 13, + "authorEmail": null + } + ] + }, + { + "category": "Account Data", + "element": "Account Password", + "dataSafetyElement": "", + "dataSafetyCategory": "", + "sensitive": false, + "sensitivity": "high", + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 1.3, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "Password", + "fileName": "apis/views/user/register.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t\t\tinput.form-control(type=\"text\", name=\"email\", value=\"\")\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right\n\t\t\t\t\t\t\t\t| Password\n\t\t\t\t\t\t\t\tspan.text-red *\n\t\t\t\t\t\t\t\t\t| :\n\t\t\t\t\t\t\t.col-md-6\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 10, + "endIndex": 18 + } + ], + "startLocationLine": 20, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "password", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right New password:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='password', name='password', value='')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Confirm New password:\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 1, + "startIndex": 51, + "endIndex": 59 + }, + { + "lineNumber": 3, + "startIndex": 33, + "endIndex": 41 + }, + { + "lineNumber": 3, + "startIndex": 50, + "endIndex": 58 + }, + { + "lineNumber": 6, + "startIndex": 59, + "endIndex": 67 + } + ], + "startLocationLine": 27, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "password", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t\t\tinput.form-control(type='password', name='old_password', value='')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right New password:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='password', name='password', value='')\n\t\t\t\t\t\t\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 0, + "startIndex": 33, + "endIndex": 41 + }, + { + "lineNumber": 0, + "startIndex": 54, + "endIndex": 62 + }, + { + "lineNumber": 3, + "startIndex": 51, + "endIndex": 59 + }, + { + "lineNumber": 5, + "startIndex": 33, + "endIndex": 41 + }, + { + "lineNumber": 5, + "startIndex": 50, + "endIndex": 58 + } + ], + "startLocationLine": 25, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "password", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Old password:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='password', name='old_password', value='')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right New password:\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 1, + "startIndex": 51, + "endIndex": 59 + }, + { + "lineNumber": 3, + "startIndex": 33, + "endIndex": 41 + }, + { + "lineNumber": 3, + "startIndex": 54, + "endIndex": 62 + }, + { + "lineNumber": 6, + "startIndex": 51, + "endIndex": 59 + } + ], + "startLocationLine": 22, + "authorEmail": null + }, + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "password", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t\t\tinput.form-control(type='text', name='email', value='#{user.email}', disabled='')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Old password:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='password', name='old_password', value='')\n\t\t\t\t\t\t\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 51, + "endIndex": 59 + }, + { + "lineNumber": 5, + "startIndex": 33, + "endIndex": 41 + }, + { + "lineNumber": 5, + "startIndex": 54, + "endIndex": 62 + } + ], + "startLocationLine": 20, + "authorEmail": null + } + ] + }, + { + "category": "User Content Data", + "element": "Ratings", + "dataSafetyElement": "Other user-generated content", + "dataSafetyCategory": "App activity", + "sensitive": false, + "sensitivity": "medium", + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "occurrences": [ + { + "score": 1.0, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Positive Word Matched" + ], + "sample": "product.rating", + "fileName": "lib/screens/details/details_screen.dart", + "language": "dart", + "excerpt": " ModalRoute.of(context).settings.arguments;\n return Scaffold(\n backgroundColor: Color(0xFFF5F6F9),\n appBar: CustomAppBar(rating: agrs.product.rating),\n body: Body(product: agrs.product),\n );\n }\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 40, + "endIndex": 54 + } + ], + "startLocationLine": 16, + "authorEmail": null + }, + { + "score": 0.75, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Positive Word Matched" + ], + "sample": "rating", + "fileName": "lib/models/Product.dart", + "language": "dart", + "excerpt": " @required this.id,\n @required this.images,\n @required this.colors,\n this.rating = 0.0,\n this.isFavourite = false,\n this.isPopular = false,\n @required this.title,\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 9, + "endIndex": 15 + } + ], + "startLocationLine": 15, + "authorEmail": null + }, + { + "score": 0.75, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Positive Word Matched" + ], + "sample": "rating", + "fileName": "lib/models/Product.dart", + "language": "dart", + "excerpt": " title: \"Wireless Controller for PS4\u2122\",\n price: 64.99,\n description: description,\n rating: 4.8,\n isFavourite: true,\n isPopular: true,\n ),\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 4, + "endIndex": 10 + } + ], + "startLocationLine": 44, + "authorEmail": null + }, + { + "score": 0.75, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Positive Word Matched" + ], + "sample": "rating", + "fileName": "lib/models/Product.dart", + "language": "dart", + "excerpt": " title: \"Nike Sport White - Man Pant\",\n price: 50.5,\n description: description,\n rating: 4.1,\n isPopular: true,\n ),\n Product(\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 4, + "endIndex": 10 + } + ], + "startLocationLine": 62, + "authorEmail": null + } + ] + }, + { + "category": "Personal Identification", + "element": "Photograph", + "dataSafetyElement": "Photos", + "dataSafetyCategory": "Photos or videos", + "sensitive": false, + "sensitivity": "medium", + "score": 1, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Function Regex Matched", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 1, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Function Regex Matched", + "CaseDependant Matched" + ], + "sample": "ProfilePic", + "fileName": "lib/screens/profile/components/body.dart", + "language": "dart", + "excerpt": " padding: EdgeInsets.symmetric(vertical: 20),\n child: Column(\n children: [\n ProfilePic(),\n SizedBox(height: 20),\n ProfileMenu(\n text: \"My Account\",\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 10, + "endIndex": 20 + } + ], + "startLocationLine": 13, + "authorEmail": null + }, + { + "score": 1, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Function Regex Matched", + "CaseDependant Matched" + ], + "sample": "ProfilePic", + "fileName": "lib/screens/profile/components/profile_pic.dart", + "language": "dart", + "excerpt": "import 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\n\nclass ProfilePic extends StatelessWidget {\n const ProfilePic({\n Key key,\n }) : super(key: key);\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 6, + "endIndex": 16 + }, + { + "lineNumber": 4, + "startIndex": 8, + "endIndex": 18 + } + ], + "startLocationLine": 4, + "authorEmail": null + }, + { + "score": 1, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "Function Regex Matched", + "CaseDependant Matched" + ], + "sample": "ProfilePic", + "fileName": "lib/screens/profile/components/profile_pic.dart", + "language": "dart", + "excerpt": "import 'package:flutter_svg/flutter_svg.dart';\n\nclass ProfilePic extends StatelessWidget {\n const ProfilePic({\n Key key,\n }) : super(key: key);\n\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 2, + "startIndex": 6, + "endIndex": 16 + }, + { + "lineNumber": 3, + "startIndex": 8, + "endIndex": 18 + } + ], + "startLocationLine": 5, + "authorEmail": null + } + ], + "markedAs": "confirmed" + }, + { + "category": "Online Identifiers", + "element": "Advertising Identifiers", + "dataSafetyElement": "Device or other identifiers", + "dataSafetyCategory": "Device or other identifiers", + "sensitive": false, + "sensitivity": "low", + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "AdvertisingId", + "fileName": "jdata.py", + "language": "python", + "excerpt": "\n } catch (IOException e) {\n // Unrecoverable error connecting to Google Play services (e.g.,\n // the old version of the service doesn't support getting AdvertisingId).\n\n } catch (GooglePlayServicesNotAvailableException e) {\n // Google Play services is not available entirely.\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 70, + "endIndex": 83 + } + ], + "startLocationLine": 7, + "authorEmail": null + }, + { + "score": 0.7000000000000001, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Variable Regex Matched", + "Getter/Setter Regex Matched", + "CaseDependant Matched" + ], + "sample": "AdvertisingId", + "fileName": "jdata.py", + "language": "python", + "excerpt": " Info adInfo = null;\n try {\n adInfo = AdvertisingIdClient.getAdvertisingIdInfo(getApplicationContext());\n\n } catch (IOException e) {\n // Unrecoverable error connecting to Google Play services (e.g.,\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 2, + "startIndex": 21, + "endIndex": 34 + }, + { + "lineNumber": 2, + "startIndex": 44, + "endIndex": 57 + } + ], + "startLocationLine": 3, + "authorEmail": null + } + ] + }, + { + "category": "Contact Data", + "element": "Phone Number", + "dataSafetyElement": "Phone number", + "dataSafetyCategory": "Personal info", + "sensitive": false, + "sensitivity": "medium", + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "phone_number", + "fileName": "apis/routes/user.js", + "language": "javascript", + "excerpt": "\t\t\t\t'password' : md5(req.body.password),\n\t\t\t\t'first_name' : req.body.first_name,\n\t\t\t\t'last_name' : req.body.last_name,\n\t\t\t\t'phone_number' : req.body.phone_number\n\t\t\t\t\n\t\t\t}, function (err, user) {\n\t\t\t\tif (err) {\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 5, + "endIndex": 17 + }, + { + "lineNumber": 3, + "startIndex": 30, + "endIndex": 42 + } + ], + "startLocationLine": 66, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "Phone", + "fileName": "apis/views/user/account.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t\t\tinput.form-control(type='last_name', name='last_name', value='#{user.last_name}')\n\t\t\t\t\t\t\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Phone Number:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type='phone_number', name='phone_number', value='#{user.phone_number}')\n\t\t\t\t\t\t\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 47, + "endIndex": 52 + } + ], + "startLocationLine": 45, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "Phone", + "fileName": "apis/views/user/register.jade", + "language": "jade", + "excerpt": "\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type=\"last_name\", name=\"last_name\", value=\"\")\n\t\t\t\t\t\t.form-group.clearfix\n\t\t\t\t\t\t\tlabel.col-md-4.control-label.text-right Phone Number:\n\t\t\t\t\t\t\t.col-md-6\n\t\t\t\t\t\t\t\tinput.form-control(type=\"phone_number\", name=\"phone_number\", value=\"\")\n\t\t\t\t\t\t.col-md-6.col-md-offset-4\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 47, + "endIndex": 52 + } + ], + "startLocationLine": 38, + "authorEmail": null + } + ] + }, + { + "category": "Audio, Visual & Sensory Data", + "element": "IoT or Sensor Data", + "dataSafetyElement": "", + "dataSafetyCategory": "", + "sensitive": false, + "sensitivity": "medium", + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "Accelerometer", + "fileName": "android/app/src/profile/analytics.java", + "language": "java", + "excerpt": "\n private TiltMazesDBAdapter mDB;\n\n private final SensorListener mSensorAccelerometer = new SensorListener() {\n\n public void onSensorChanged(int sensor, float[] values) {\n if (!mSensorEnabled) return;\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 40, + "endIndex": 53 + } + ], + "startLocationLine": 84, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "Accelerometer", + "fileName": "android/app/src/profile/analytics.java", + "language": "java", + "excerpt": "\n // Register the sensor listener\n mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);\n mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER,\n SensorManager.SENSOR_DELAY_GAME);\n\n mMap = new Map(MapDesigns.designList.get(mCurrentMap));\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 47, + "endIndex": 60 + } + ], + "startLocationLine": 124, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "ACCELEROMETER", + "fileName": "android/app/src/profile/analytics.java", + "language": "java", + "excerpt": "\n // Register the sensor listener\n mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);\n mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER,\n SensorManager.SENSOR_DELAY_GAME);\n\n mMap = new Map(MapDesigns.designList.get(mCurrentMap));\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 83, + "endIndex": 96 + } + ], + "startLocationLine": 124, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "Accelerometer", + "fileName": "android/app/src/profile/analytics.java", + "language": "java", + "excerpt": " }\n\n public void registerListener() {\n mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER,\n SensorManager.SENSOR_DELAY_GAME);\n }\n\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 47, + "endIndex": 60 + } + ], + "startLocationLine": 302, + "authorEmail": null + }, + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "high confidence", + "CaseDependant Matched" + ], + "sample": "ACCELEROMETER", + "fileName": "android/app/src/profile/analytics.java", + "language": "java", + "excerpt": " }\n\n public void registerListener() {\n mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER,\n SensorManager.SENSOR_DELAY_GAME);\n }\n\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 83, + "endIndex": 96 + } + ], + "startLocationLine": 302, + "authorEmail": null + } + ] + }, + { + "category": "Online Identifiers", + "element": "Cookies", + "dataSafetyElement": "Device or other identifiers", + "dataSafetyCategory": "Device or other identifiers", + "sensitive": false, + "sensitivity": "medium", + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "occurrences": [ + { + "score": 0.8, + "confidence": "high", + "scoringPoints": [ + "medium confidence", + "Positive Word Matched", + "CaseDependant Matched" + ], + "sample": "cookie", + "fileName": "apis/app.js", + "language": "app-permissions", + "excerpt": "app.use(logger('dev'));\napp.use(bodyParser.json());\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(cookieParser());\napp.use(express.static(path.join(__dirname, 'public')));\napp.use(flash());\napp.use(session({\n", + "isCorrect": null, + "occurrenceIndexes": [ + { + "lineNumber": 3, + "startIndex": 8, + "endIndex": 14 + } + ], + "startLocationLine": 52, + "authorEmail": null + } + ] + }, + { + "category": "Location Data", + "element": "Precise Location", + "dataSafetyElement": "Precise location", + "dataSafetyCategory": "Location", + "sensitive": false, + "sensitivity": "high", + "score": 0.5, + "confidence": "low", + "scoringPoints": [ + "high confidence" + ], + "occurrences": [ + { + "score": 1, + "confidence": "high", + "scoringPoints": [], + "sample": "android.permission.ACCESS_COARSE_LOCATION", + "fileName": "AndroidManifest.xml", + "language": "app-permissions", + "excerpt": " \n\n \n \n \n\n \n \n \n\n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/build.gradle b/android/app/build.gradle index 36fee475..46a86abd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -60,4 +60,12 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.stripe:stripe-android:14.1.0' + implementation 'com.google.android.gms:play-services-ads:20.5.0' + implementation 'com.paypal.sdk:paypal-android-sdk:2.14.2' + implementation 'com.google.android.gms.fonts' + if (amplitudeUseLocal.toBoolean()) { + compile files('libs/amplitude-android-' + amplitudeVersion + '-with-dependencies.jar') + } else { + compile 'com.amplitude:android-sdk:' + amplitudeVersion } diff --git a/android/app/src/profile/ActivityAds.java b/android/app/src/profile/ActivityAds.java new file mode 100644 index 00000000..25e213e4 --- /dev/null +++ b/android/app/src/profile/ActivityAds.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2013 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.gms.example.interstitialexample; + +import android.os.Bundle; +import android.os.CountDownTimer; +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import com.google.android.gms.ads.AdError; +import com.google.android.gms.ads.AdRequest; +import com.google.android.gms.ads.FullScreenContentCallback; +import com.google.android.gms.ads.LoadAdError; +import com.google.android.gms.ads.MobileAds; +import com.google.android.gms.ads.initialization.InitializationStatus; +import com.google.android.gms.ads.initialization.OnInitializationCompleteListener; +import com.google.android.gms.ads.interstitial.InterstitialAd; +import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback; + +/** + * Main Activity. Inflates main activity xml. + */ +public class MyActivity extends AppCompatActivity { + + private static final long GAME_LENGTH_MILLISECONDS = 3000; + private static final String AD_UNIT_ID = "ca-app-pub-3940256099942544/1033173712"; + private static final String TAG = "MyActivity"; + + private InterstitialAd interstitialAd; + private CountDownTimer countDownTimer; + private Button retryButton; + private boolean gameIsInProgress; + private long timerMilliseconds; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_my); + + // Initialize the Mobile Ads SDK. + MobileAds.initialize(this, new OnInitializationCompleteListener() { + @Override + public void onInitializationComplete(InitializationStatus initializationStatus) {} + }); + + loadAd(); + + // Create the "retry" button, which tries to show an interstitial between game plays. + retryButton = findViewById(R.id.retry_button); + retryButton.setVisibility(View.INVISIBLE); + retryButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showInterstitial(); + } + }); + + startGame(); + } + + public void loadAd() { + AdRequest adRequest = new AdRequest.Builder().build(); + InterstitialAd.load( + this, + AD_UNIT_ID, + adRequest, + new InterstitialAdLoadCallback() { + @Override + public void onAdLoaded(@NonNull InterstitialAd interstitialAd) { + // The mInterstitialAd reference will be null until + // an ad is loaded. + MyActivity.this.interstitialAd = interstitialAd; + Log.i(TAG, "onAdLoaded"); + Toast.makeText(MyActivity.this, "onAdLoaded()", Toast.LENGTH_SHORT).show(); + interstitialAd.setFullScreenContentCallback( + new FullScreenContentCallback() { + @Override + public void onAdDismissedFullScreenContent() { + // Called when fullscreen content is dismissed. + // Make sure to set your reference to null so you don't + // show it a second time. + MyActivity.this.interstitialAd = null; + Log.d("TAG", "The ad was dismissed."); + } + + @Override + public void onAdFailedToShowFullScreenContent(AdError adError) { + // Called when fullscreen content failed to show. + // Make sure to set your reference to null so you don't + // show it a second time. + MyActivity.this.interstitialAd = null; + Log.d("TAG", "The ad failed to show."); + } + + @Override + public void onAdShowedFullScreenContent() { + // Called when fullscreen content is shown. + Log.d("TAG", "The ad was shown."); + } + }); + } + + @Override + public void onAdFailedToLoad(@NonNull LoadAdError loadAdError) { + // Handle the error + Log.i(TAG, loadAdError.getMessage()); + interstitialAd = null; + + String error = + String.format( + "domain: %s, code: %d, message: %s", + loadAdError.getDomain(), loadAdError.getCode(), loadAdError.getMessage()); + Toast.makeText( + MyActivity.this, "onAdFailedToLoad() with error: " + error, Toast.LENGTH_SHORT) + .show(); + } + }); + } + + private void createTimer(final long milliseconds) { + // Create the game timer, which counts down to the end of the level + // and shows the "retry" button. + if (countDownTimer != null) { + countDownTimer.cancel(); + } + + final TextView textView = findViewById(R.id.timer); + + countDownTimer = new CountDownTimer(milliseconds, 50) { + @Override + public void onTick(long millisUnitFinished) { + timerMilliseconds = millisUnitFinished; + textView.setText("seconds remaining: " + ((millisUnitFinished / 1000) + 1)); + } + + @Override + public void onFinish() { + gameIsInProgress = false; + textView.setText("done!"); + retryButton.setVisibility(View.VISIBLE); + } + }; + } + + @Override + public void onResume() { + // Start or resume the game. + super.onResume(); + + if (gameIsInProgress) { + resumeGame(timerMilliseconds); + } + } + + @Override + public void onPause() { + // Cancel the timer if the game is paused. + countDownTimer.cancel(); + super.onPause(); + } + + private void showInterstitial() { + // Show the ad if it's ready. Otherwise toast and restart the game. + if (interstitialAd != null) { + interstitialAd.show(this); + } else { + Toast.makeText(this, "Ad did not load", Toast.LENGTH_SHORT).show(); + startGame(); + } + } + + private void startGame() { + // Request a new ad if one isn't already loaded, hide the button, and kick off the timer. + if (interstitialAd == null) { + loadAd(); + } + + retryButton.setVisibility(View.INVISIBLE); + resumeGame(GAME_LENGTH_MILLISECONDS); + } + + private void resumeGame(long milliseconds) { + // Create a new timer for the correct length and start it. + gameIsInProgress = true; + timerMilliseconds = milliseconds; + createTimer(milliseconds); + countDownTimer.start(); + } +} diff --git a/android/app/src/profile/Checkoutactivity.java b/android/app/src/profile/Checkoutactivity.java new file mode 100644 index 00000000..781f0bd7 --- /dev/null +++ b/android/app/src/profile/Checkoutactivity.java @@ -0,0 +1,227 @@ +package com.example.app; + +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.android.gms.fonts; +import com.google.gson.reflect.TypeToken; +import com.stripe.android.ApiResultCallback; +import com.stripe.android.PaymentIntentResult; +import com.stripe.android.Stripe; +import com.stripe.android.model.ConfirmPaymentIntentParams; +import com.stripe.android.model.PaymentIntent; +import com.stripe.android.model.PaymentMethodCreateParams; +import com.stripe.android.view.CardInputWidget; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Objects; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class CheckoutActivityJava extends AppCompatActivity { + /** + * + * This example collects card payments, implementing the guide here: https://stripe.com/docs/payments/accept-a-payment#android + * + * To run this app, follow the steps here: https://github.com/stripe-samples/accept-a-card-payment#how-to-run-locally + */ + // 10.0.2.2 is the Android emulator's alias to localhost + private static final String BACKEND_URL = "http://10.0.2.2:4242/"; + + private OkHttpClient httpClient = new OkHttpClient(); + private String paymentIntentClientSecret; + private Stripe stripe; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_checkout); + startCheckout(); + } + + private void startCheckout() { + // Create a PaymentIntent by calling the sample server's /create-payment-intent endpoint. + MediaType mediaType = MediaType.get("application/json; charset=utf-8"); + String json = "{" + + "\"currency\":\"usd\"," + + "\"items\":[" + + "{\"id\":\"photo_subscription\"}" + + "]" + + "}"; + RequestBody body = RequestBody.create(json, mediaType); + Request request = new Request.Builder() + .url(BACKEND_URL + "create-payment-intent") + .post(body) + .build(); + httpClient.newCall(request) + .enqueue(new PayCallback(this)); + + // Hook up the pay button to the card widget and stripe instance + Button payButton = findViewById(R.id.payButton); + payButton.setOnClickListener((View view) -> { + CardInputWidget cardInputWidget = findViewById(R.id.cardInputWidget); + PaymentMethodCreateParams params = cardInputWidget.getPaymentMethodCreateParams(); + if (params != null) { + ConfirmPaymentIntentParams confirmParams = ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams(params, paymentIntentClientSecret); + stripe.confirmPayment(this, confirmParams); + } + }); + } + + private void displayAlert(@NonNull String title, + @Nullable String message, + boolean restartDemo) { + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message); + if (restartDemo) { + builder.setPositiveButton("Restart demo", + (DialogInterface dialog, int index) -> { + CardInputWidget cardInputWidget = findViewById(R.id.cardInputWidget); + cardInputWidget.clear(); + startCheckout(); + }); + } else { + builder.setPositiveButton("Ok", null); + } + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + // Handle the result of stripe.confirmPayment + stripe.onPaymentResult(requestCode, data, new PaymentResultCallback(this)); + } + + private void onPaymentSuccess(@NonNull final Response response) throws IOException { + Gson gson = new Gson(); + Type type = new TypeToken>(){}.getType(); + Map responseMap = gson.fromJson( + Objects.requireNonNull(response.body()).string(), + type + ); + + // The response from the server includes the Stripe publishable key and + // PaymentIntent details. + // For added security, our sample app gets the publishable key from the server + String stripePublishableKey = responseMap.get("publishableKey"); + paymentIntentClientSecret = responseMap.get("clientSecret"); + + // Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API + stripe = new Stripe( + getApplicationContext(), + Objects.requireNonNull(stripePublishableKey) + ); + } + + private static final class PayCallback implements Callback { + @NonNull private final WeakReference activityRef; + + PayCallback(@NonNull CheckoutActivityJava activity) { + activityRef = new WeakReference<>(activity); + } + + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + final CheckoutActivityJava activity = activityRef.get(); + if (activity == null) { + return; + } + + activity.runOnUiThread(() -> + Toast.makeText( + activity, "Error: " + e.toString(), Toast.LENGTH_LONG + ).show() + ); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull final Response response) + throws IOException { + final CheckoutActivityJava activity = activityRef.get(); + if (activity == null) { + return; + } + + if (!response.isSuccessful()) { + activity.runOnUiThread(() -> + Toast.makeText( + activity, "Error: " + response.toString(), Toast.LENGTH_LONG + ).show() + ); + } else { + activity.onPaymentSuccess(response); + } + } + } + + private static final class PaymentResultCallback + implements ApiResultCallback { + @NonNull private final WeakReference activityRef; + + PaymentResultCallback(@NonNull CheckoutActivityJava activity) { + activityRef = new WeakReference<>(activity); + } + + @Override + public void onSuccess(@NonNull PaymentIntentResult result) { + final CheckoutActivityJava activity = activityRef.get(); + if (activity == null) { + return; + } + + PaymentIntent paymentIntent = result.getIntent(); + PaymentIntent.Status status = paymentIntent.getStatus(); + if (status == PaymentIntent.Status.Succeeded) { + // Payment completed successfully + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + activity.displayAlert( + "Payment completed", + gson.toJson(paymentIntent), + true + ); + } else if (status == PaymentIntent.Status.RequiresPaymentMethod) { + // Payment failed – allow retrying using a different payment method + activity.displayAlert( + "Payment failed", + Objects.requireNonNull(paymentIntent.getLastPaymentError()).getMessage(), + false + ); + } + } + + @Override + public void onError(@NonNull Exception e) { + final CheckoutActivityJava activity = activityRef.get(); + if (activity == null) { + return; + } + + // Payment request failed – allow retrying using the same payment method + activity.displayAlert("Error", e.toString(), false); + } + } +} diff --git a/android/app/src/profile/analytics.java b/android/app/src/profile/analytics.java new file mode 100644 index 00000000..48078d20 --- /dev/null +++ b/android/app/src/profile/analytics.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2009, Balazs Lecz + * Copyright (c) 2015, Amplitude Mobile Analytics + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the names of Balazs Lecz or Amplitude Mobile Analytics nor the names of + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +package com.lecz.android.tiltmazes; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.hardware.SensorListener; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Vibrator; +import android.widget.TextView; + +import com.amplitude.api.Amplitude; +import com.amplitude.api.Identify; + +import org.json.JSONException; +import org.json.JSONObject; + +public class GameEngine { + private SensorManager mSensorManager; + private Vibrator mVibrator; + + private static float ACCEL_THRESHOLD = 2; + private float mAccelX = 0; + private float mAccelY = 0; + @SuppressWarnings("unused") + private float mAccelZ = 0; + + private Handler mHandler; + + private Map mMap; + private Ball mBall; + private int mCurrentMap = 0; + private int mMapToLoad = 0; + private int mStepCount = 0; + + private Direction mCommandedRollDirection = Direction.NONE; + + private TextView mMazeNameLabel; + private TextView mRemainingGoalsLabel; + private TextView mStepsView; + private MazeView mMazeView; + + private final AlertDialog mMazeSolvedDialog; + private final AlertDialog mAllMazesSolvedDialog; + + private boolean mSensorEnabled = true; + + private TiltMazesDBAdapter mDB; + + private final SensorListener mSensorAccelerometer = new SensorListener() { + + public void onSensorChanged(int sensor, float[] values) { + if (!mSensorEnabled) return; + + mAccelX = values[0]; + mAccelY = values[1]; + mAccelZ = values[2]; + + mCommandedRollDirection = Direction.NONE; + if (Math.abs(mAccelX) > Math.abs(mAccelY)) { + if (mAccelX < -ACCEL_THRESHOLD) mCommandedRollDirection = Direction.LEFT; + // FIXME(leczbalazs) elseif + if (mAccelX > ACCEL_THRESHOLD) mCommandedRollDirection = Direction.RIGHT; + } else { + if (mAccelY < -ACCEL_THRESHOLD) mCommandedRollDirection = Direction.DOWN; + // FIXME(leczbalazs) elseif + if (mAccelY > ACCEL_THRESHOLD) mCommandedRollDirection = Direction.UP; + } + + if (mCommandedRollDirection != Direction.NONE && !mBall.isRolling()) { + rollBall(mCommandedRollDirection); + } + } + + public void onAccuracyChanged(int sensor, int accuracy) { + } + }; + + + public GameEngine(Context context) { + // Open maze database + mDB = new TiltMazesDBAdapter(context).open(); + mCurrentMap = mDB.getFirstUnsolved(); + + // Request vibrator service + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + + // Register the sensor listener + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER, + SensorManager.SENSOR_DELAY_GAME); + + mMap = new Map(MapDesigns.designList.get(mCurrentMap)); + + // Create ball + mBall = new Ball(this, mMap, + mMap.getInitialPositionX(), + mMap.getInitialPositionY()); + + // Congratulations dialog + mMazeSolvedDialog = new AlertDialog.Builder(context) + .setCancelable(true) + .setIcon(android.R.drawable.ic_dialog_info) + .setTitle("Congratulations!") + .setPositiveButton("Go to next maze!", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.cancel(); + sendEmptyMessage(Messages.MSG_MAP_NEXT); + } + }) + .create(); + + // Final congratulations dialog + mAllMazesSolvedDialog = new AlertDialog.Builder(context) + .setCancelable(true) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle("Congratulations!") + .setPositiveButton("OK!", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.cancel(); + sendEmptyMessage(Messages.MSG_MAP_NEXT); + } + }) + .create(); + + // Create message handler + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case Messages.MSG_INVALIDATE: + mMazeView.invalidate(); + return; + + case Messages.MSG_REACHED_GOAL: + mRemainingGoalsLabel.setText("" + mMap.getGoalCount()); + mRemainingGoalsLabel.invalidate(); + vibrate(100); + + if (mMap.getGoalCount() == 0) { + // Solved! + mDB.updateMaze(mCurrentMap, mStepCount); + + // Update the user properties with number of mazes completed. + JSONObject userProperties = new JSONObject(); + try { + userProperties.put("Mazes Completed", mDB.solvedMazes().getCount()); + } catch (JSONException exception) { + } + Amplitude.getInstance().setUserProperties(userProperties); + + // Track the maze completion in amplitude + JSONObject eventProperties = new JSONObject(); + try { + eventProperties.put("Maze", mMap.getName()); + eventProperties.put("Steps", mStepCount); + } catch (JSONException exception) { + } + + Amplitude.getInstance().logEvent("Maze Completed", eventProperties); + Amplitude.getInstance().identify(new Identify().add("total steps", mStepCount)); + Amplitude.getInstance().identify(new Identify().add("mazes completed", 1)); + Long tsLong = System.currentTimeMillis() / 1000; + Amplitude.getInstance().identify(new Identify().setOnce("first maze completed time", tsLong)); + + if (mDB.unsolvedMazes().getCount() == 0) { + mAllMazesSolvedDialog.setMessage( + "Mad props!\nYou have solved all the mazes!\n" + + "Now go back and try to solve them in fewer steps! :)"); + mAllMazesSolvedDialog.show(); + } else { + mMazeSolvedDialog.setMessage( + "You have solved maze " + + mMap.getName() + + " in " + mStepCount + " steps." + ); + mMazeSolvedDialog.show(); + } + } + return; + + case Messages.MSG_REACHED_WALL: + vibrate(12); + return; + + case Messages.MSG_RESTART: + loadMap(mCurrentMap); + return; + + case Messages.MSG_MAP_PREVIOUS: + case Messages.MSG_MAP_NEXT: + switch (msg.what) { + case (Messages.MSG_MAP_PREVIOUS): + if (mCurrentMap == 0) { + // Wrap around + mMapToLoad = MapDesigns.designList.size() - 1; + } else { + mMapToLoad = (mCurrentMap - 1) % MapDesigns.designList.size(); + } + break; + + case (Messages.MSG_MAP_NEXT): + mMapToLoad = (mCurrentMap + 1) % MapDesigns.designList.size(); + break; + } + + loadMap(mMapToLoad); + return; + } + + super.handleMessage(msg); + } + }; + } + + public void loadMap(int mapID) { + mCurrentMap = mapID; + mBall.stop(); + mMap = new Map(MapDesigns.designList.get(mCurrentMap)); + mBall.setMap(mMap); + mBall.setX(mMap.getInitialPositionX()); + mBall.setY(mMap.getInitialPositionY()); + mBall.setXTarget(mMap.getInitialPositionX()); + mBall.setYTarget(mMap.getInitialPositionY()); + mMap.init(); + + mStepCount = 0; + + mMazeNameLabel.setText(mMap.getName()); + mMazeNameLabel.invalidate(); + + mRemainingGoalsLabel.setText("" + mMap.getGoalCount()); + mRemainingGoalsLabel.invalidate(); + + mStepsView.setText("" + mStepCount); + mStepsView.invalidate(); + + mMazeView.calculateUnit(); + mMazeView.invalidate(); + } + + public void setMazeNameLabel(TextView mazeNameLabel) { + mMazeNameLabel = mazeNameLabel; + } + + public void setRemainingGoalsLabel(TextView remainingGoalsLabel) { + mRemainingGoalsLabel = remainingGoalsLabel; + } + + public void setTiltMazesView(MazeView mazeView) { + mMazeView = mazeView; + mBall.setMazeView(mazeView); + } + + public void setStepsLabel(TextView stepsView) { + mStepsView = stepsView; + } + + public void sendEmptyMessage(int msg) { + mHandler.sendEmptyMessage(msg); + } + + public void sendMessage(Message msg) { + mHandler.sendMessage(msg); + } + + public void registerListener() { + mSensorManager.registerListener(mSensorAccelerometer, SensorManager.SENSOR_ACCELEROMETER, + SensorManager.SENSOR_DELAY_GAME); + } + + public void unregisterListener() { + mSensorManager.unregisterListener(mSensorAccelerometer); + } + + public void rollBall(Direction dir) { + if (mBall.roll(dir)) mStepCount++; + mStepsView.setText("" + mStepCount); + mStepsView.invalidate(); + } + + public Ball getBall() { + return mBall; + } + + public Map getMap() { + return mMap; + } + + public boolean isSensorEnabled() { + return mSensorEnabled; + } + + public void toggleSensorEnabled() { + mSensorEnabled = !mSensorEnabled; + } + + public void vibrate(long milliseconds) { + mVibrator.vibrate(milliseconds); + } + + public void saveState(Bundle icicle) { + mBall.stop(); + + icicle.putInt("map.id", mCurrentMap); + + int[][] goals = mMap.getGoals(); + int sizeX = mMap.getSizeX(); + int sizeY = mMap.getSizeY(); + int[] goalsToSave = new int[sizeX * sizeY]; + for (int y = 0; y < sizeY; y++) + for (int x = 0; x < sizeX; x++) + goalsToSave[y + x * sizeX] = goals[y][x]; + icicle.putIntArray("map.goals", goalsToSave); + + icicle.putInt("stepcount", mStepCount); + + icicle.putInt("ball.x", Math.round(mBall.getX())); + icicle.putInt("ball.y", Math.round(mBall.getY())); + } + + public void restoreState(Bundle icicle, boolean sensorEnabled) { + if (icicle != null) { + int mapID = icicle.getInt("map.id", -1); + if (mapID == -1) return; + loadMap(mapID); + + int[] goals = icicle.getIntArray("map.goals"); + if (goals == null) return; + + int sizeX = mMap.getSizeX(); + int sizeY = mMap.getSizeY(); + for (int y = 0; y < sizeY; y++) + for (int x = 0; x < sizeX; x++) + mMap.setGoal(x, y, goals[y + x * sizeX]); + + mBall.setX(icicle.getInt("ball.x")); + mBall.setY(icicle.getInt("ball.y")); + + // We have probably moved the ball, so invalidate the Maze View + mMazeView.invalidate(); + + mStepCount = icicle.getInt("stepcount", 0); + } + mRemainingGoalsLabel.setText("" + mMap.getGoalCount()); + mRemainingGoalsLabel.invalidate(); + + mStepsView.setText("" + mStepCount); + mStepsView.invalidate(); + + mSensorEnabled = sensorEnabled; + } +} diff --git a/android/app/src/profile/payment.java b/android/app/src/profile/payment.java new file mode 100644 index 00000000..d791f797 --- /dev/null +++ b/android/app/src/profile/payment.java @@ -0,0 +1,307 @@ +package com.paypal.example.paypalandroidsdkexample + +import com.paypal.android.sdk.payments.PayPalAuthorization +import com.paypal.android.sdk.payments.PayPalConfiguration +import com.paypal.android.sdk.payments.PayPalFuturePaymentActivity +import com.paypal.android.sdk.payments.PayPalItem +import com.paypal.android.sdk.payments.PayPalOAuthScopes +import com.paypal.android.sdk.payments.PayPalPayment +import com.paypal.android.sdk.payments.PayPalPaymentDetails +import com.paypal.android.sdk.payments.PayPalProfileSharingActivity +import com.paypal.android.sdk.payments.PayPalService +import com.paypal.android.sdk.payments.PaymentActivity +import com.paypal.android.sdk.payments.PaymentConfirmation +import com.paypal.android.sdk.payments.ShippingAddress + + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.TextView +import android.widget.Toast + +import org.json.JSONException + +import java.math.BigDecimal +import java.util.Arrays +import java.util.HashSet + +/** + * Basic sample using the SDK to make a payment or consent to future payments. + * + * For sample mobile backend interactions, see + * https://github.com/paypal/rest-api-sdk-python/tree/master/samples/mobile_backend + */ +class SampleActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val intent = Intent(this, PayPalService::class.java) + intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config) + startService(intent) + } + + fun onBuyPressed(pressed: View) { + /* + * PAYMENT_INTENT_SALE will cause the payment to complete immediately. + * Change PAYMENT_INTENT_SALE to + * - PAYMENT_INTENT_AUTHORIZE to only authorize payment and capture funds later. + * - PAYMENT_INTENT_ORDER to create a payment for authorization and capture + * later via calls from your server. + * + * Also, to include additional payment details and an item list, see getStuffToBuy() below. + */ + val thingToBuy = getThingToBuy(PayPalPayment.PAYMENT_INTENT_SALE) + + /* + * See getStuffToBuy(..) for examples of some available payment options. + */ + + val intent = Intent(this@SampleActivity, PaymentActivity::class.java) + + // send the same configuration for restart resiliency + intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config) + + intent.putExtra(PaymentActivity.EXTRA_PAYMENT, thingToBuy) + + startActivityForResult(intent, REQUEST_CODE_PAYMENT) + } + + private fun getThingToBuy(paymentIntent: String): PayPalPayment { + return PayPalPayment(BigDecimal("1.75"), "USD", "sample item", + paymentIntent) + } + + /* + * This method shows use of optional payment details and item list. + */ + private fun getStuffToBuy(paymentIntent: String): PayPalPayment { + //--- include an item list, payment amount details + val items = arrayOf( + PayPalItem("sample item #1", 2, BigDecimal("87.50"), "USD", "sku-12345678"), + PayPalItem("free sample item #2", 1, BigDecimal("0.00"), "USD", "sku-zero-price"), + PayPalItem("sample item #3 with a longer name", 6, BigDecimal("37.99"), "USD", "sku-33333") + ) + val subtotal = PayPalItem.getItemTotal(items) + val shipping = BigDecimal("7.21") + val tax = BigDecimal("4.67") + val paymentDetails = PayPalPaymentDetails(shipping, subtotal, tax) + val amount = subtotal.add(shipping).add(tax) + val payment = PayPalPayment(amount, "USD", "sample item", paymentIntent) + payment.items(items).paymentDetails(paymentDetails) + + //--- set other optional fields like invoice_number, custom field, and soft_descriptor + payment.custom("This is text that will be associated with the payment that the app can use.") + + return payment + } + + /* + * Add app-provided shipping address to payment + */ + private fun addAppProvidedShippingAddress(paypalPayment: PayPalPayment) { + val shippingAddress = ShippingAddress() + .recipientName("Mom Parker") + .line1("52 North Main St.") + .city("Austin") + .state("TX") + .postalCode("78729") + .countryCode("US") + paypalPayment.providedShippingAddress(shippingAddress) + } + + /* + * Enable retrieval of shipping addresses from buyer's PayPal account + */ + private fun enableShippingAddressRetrieval(paypalPayment: PayPalPayment, enable: Boolean) { + paypalPayment.enablePayPalShippingAddressesRetrieval(enable) + } + + fun onFuturePaymentPressed(pressed: View) { + val intent = Intent(this@SampleActivity, PayPalFuturePaymentActivity::class.java) + + // send the same configuration for restart resiliency + intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config) + + startActivityForResult(intent, REQUEST_CODE_FUTURE_PAYMENT) + } + + fun onProfileSharingPressed(pressed: View) { + val intent = Intent(this@SampleActivity, PayPalProfileSharingActivity::class.java) + + // send the same configuration for restart resiliency + intent.putExtra(PayPalService.EXTRA_PAYPAL_CONFIGURATION, config) + + intent.putExtra(PayPalProfileSharingActivity.EXTRA_REQUESTED_SCOPES, oauthScopes) + + startActivityForResult(intent, REQUEST_CODE_PROFILE_SHARING) + } + + private /* create the set of required scopes + * Note: see https://developer.paypal.com/docs/integration/direct/identity/attributes/ for mapping between the + * attributes you select for this app in the PayPal developer portal and the scopes required here. + */ val oauthScopes: PayPalOAuthScopes + get() { + val scopes = HashSet( + Arrays.asList( + PayPalOAuthScopes.PAYPAL_SCOPE_EMAIL, + PayPalOAuthScopes.PAYPAL_SCOPE_ADDRESS + ) + ) + return PayPalOAuthScopes(scopes) + } + + protected fun displayResultText(result: String) { + var resultView:TextView = findViewById(R.id.txtResult) + resultView.text = "Result : " + result + Toast.makeText( + applicationContext, + result, Toast.LENGTH_LONG).show() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_PAYMENT) { + if (resultCode == Activity.RESULT_OK) { + val confirm = data?.getParcelableExtra(PaymentActivity.EXTRA_RESULT_CONFIRMATION) + if (confirm != null) { + try { + Log.i(TAG, confirm.toJSONObject().toString(4)) + Log.i(TAG, confirm.payment.toJSONObject().toString(4)) + /** + * TODO: send 'confirm' (and possibly confirm.getPayment() to your server for verification + * or consent completion. + * See https://developer.paypal.com/webapps/developer/docs/integration/mobile/verify-mobile-payment/ + * for more details. + * For sample mobile backend interactions, see + * https://github.com/paypal/rest-api-sdk-python/tree/master/samples/mobile_backend + */ + displayResultText("PaymentConfirmation info received from PayPal") + + + } catch (e: JSONException) { + Log.e(TAG, "an extremely unlikely failure occurred: ", e) + } + + } + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i(TAG, "The user canceled.") + } else if (resultCode == PaymentActivity.RESULT_EXTRAS_INVALID) { + Log.i( + TAG, + "An invalid Payment or PayPalConfiguration was submitted. Please see the docs.") + } + } else if (requestCode == REQUEST_CODE_FUTURE_PAYMENT) { + if (resultCode == Activity.RESULT_OK) { + val auth = data?.getParcelableExtra(PayPalFuturePaymentActivity.EXTRA_RESULT_AUTHORIZATION) + if (auth != null) { + try { + Log.i("FuturePaymentExample", auth.toJSONObject().toString(4)) + + val authorization_code = auth.authorizationCode + Log.i("FuturePaymentExample", authorization_code) + + sendAuthorizationToServer(auth) + displayResultText("Future Payment code received from PayPal") + + } catch (e: JSONException) { + Log.e("FuturePaymentExample", "an extremely unlikely failure occurred: ", e) + } + + } + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i("FuturePaymentExample", "The user canceled.") + } else if (resultCode == PayPalFuturePaymentActivity.RESULT_EXTRAS_INVALID) { + Log.i( + "FuturePaymentExample", + "Probably the attempt to previously start the PayPalService had an invalid PayPalConfiguration. Please see the docs.") + } + } else if (requestCode == REQUEST_CODE_PROFILE_SHARING) { + if (resultCode == Activity.RESULT_OK) { + val auth = data?.getParcelableExtra(PayPalProfileSharingActivity.EXTRA_RESULT_AUTHORIZATION) + if (auth != null) { + try { + Log.i("ProfileSharingExample", auth.toJSONObject().toString(4)) + + val authorization_code = auth.authorizationCode + Log.i("ProfileSharingExample", authorization_code) + + sendAuthorizationToServer(auth) + displayResultText("Profile Sharing code received from PayPal") + + } catch (e: JSONException) { + Log.e("ProfileSharingExample", "an extremely unlikely failure occurred: ", e) + } + + } + } else if (resultCode == Activity.RESULT_CANCELED) { + Log.i("ProfileSharingExample", "The user canceled.") + } else if (resultCode == PayPalFuturePaymentActivity.RESULT_EXTRAS_INVALID) { + Log.i( + "ProfileSharingExample", + "Probably the attempt to previously start the PayPalService had an invalid PayPalConfiguration. Please see the docs.") + } + } + } + + private fun sendAuthorizationToServer(authorization: PayPalAuthorization) { + + /** + * TODO: Send the authorization response to your server, where it can + * exchange the authorization code for OAuth access and refresh tokens. + * Your server must then store these tokens, so that your server code + * can execute payments for this user in the future. + * A more complete example that includes the required app-server to + * PayPal-server integration is available from + * https://github.com/paypal/rest-api-sdk-python/tree/master/samples/mobile_backend + */ + + } + + fun onFuturePaymentPurchasePressed(pressed: View) { + // Get the Client Metadata ID from the SDK + val metadataId = PayPalConfiguration.getClientMetadataId(this) + + Log.i("FuturePaymentExample", "Client Metadata ID: " + metadataId) + + // TODO: Send metadataId and transaction details to your server for processing with + // PayPal... + displayResultText("Client Metadata Id received from SDK") + } + + public override fun onDestroy() { + // Stop service when done + stopService(Intent(this, PayPalService::class.java)) + super.onDestroy() + } + + companion object { + private val TAG = "paymentExample" + /** + * - Set to PayPalConfiguration.ENVIRONMENT_PRODUCTION to move real money. + * - Set to PayPalConfiguration.ENVIRONMENT_SANDBOX to use your test credentials + * from https://developer.paypal.com + * - Set to PayPalConfiguration.ENVIRONMENT_NO_NETWORK to kick the tires + * without communicating to PayPal's servers. + */ + private val CONFIG_ENVIRONMENT = PayPalConfiguration.ENVIRONMENT_NO_NETWORK + + // note that these credentials will differ between live & sandbox environments. + private val CONFIG_CLIENT_ID = "credentials from developer.paypal.com" + + private val REQUEST_CODE_PAYMENT = 1 + private val REQUEST_CODE_FUTURE_PAYMENT = 2 + private val REQUEST_CODE_PROFILE_SHARING = 3 + + private val config = PayPalConfiguration() + .environment(CONFIG_ENVIRONMENT) + .clientId(CONFIG_CLIENT_ID) + .merchantName("Example Merchant") + .merchantPrivacyPolicyUri(Uri.parse("https://www.example.com/privacy")) + .merchantUserAgreementUri(Uri.parse("https://www.example.com/legal")) + } +} diff --git a/apis/.gitignore b/apis/.gitignore new file mode 100644 index 00000000..b8684af4 --- /dev/null +++ b/apis/.gitignore @@ -0,0 +1,4 @@ +/data/ +/node_modules/ +/public/images/uploads/ + diff --git a/apis/README.md b/apis/README.md new file mode 100644 index 00000000..ea7b0b19 --- /dev/null +++ b/apis/README.md @@ -0,0 +1,45 @@ +# Node.js E-commerce +Companies nowadays are now turning their businesses online to provide their customers a better experience. One of the most popular e-commerce website we hear often is Ebay – an American multinational company running on Node.js . A lot of folks are suggesting that Node.js might be the future of web development. Node.js is a single-threaded event-driven system that runs fast even when handling lots of requests at once, it is also simple compared to traditional multi-threaded frameworks. Node.js is well suited for real-time applications: online games, collaboration tools, chat rooms, or anything where what one user (or robot?) does with the application needs to be seen by other users immediately, without a page refresh. Using a technique known as “long-polling”, you can write an application that sends updates to the user in real time. + +## Demo +This simple e-commerce application demonstrates CRUD operations using mongoDB and a simple implementation of a session based user authentication using Passport.js. It also utilizes real time update front-end products using socket.io. This web app is 100% free to use, you can customize it to build a more sophisticated e-commerce web application. Feel free to submit an issue on GitHub if you found any bug or even better – submit a pull request. + +This web application is currently hosted on Heroku, [click here](https://nodejs-ecommerce.herokuapp.com/) to view the demo. Heroku is for testing only - it does not support file uploads as the filesystem is readonly. Although you can still test the image upload functionality but all images will automatically be deleted after a couple of minutes. + +## Modules Used ++ async ++ connect-flash ++ cookie-parser ++ crypto-md5 ++ express ++ express-session ++ jade ++ mongodb ++ monk ++ multer ++ passport ++ passport-local ++ socket.io + +## How do I get setup? +1. Create a folder called "data" in the root directory of our nodejs project. This is where MongoDB documents will be stored. +2. Go to MongoDB installation directory and under the bin folder run this command: `mongod --dbpath C:\Users\Carl\Documents\nodejs-ecommerce\data` This will start the MongoDB server. Leave this CLI instance open and start another CLI instance. +3. In the new CLI, navigate to where you pulled this repository, ex. `C:\Users\Carl\Documents\nodejs-ecommerce`, then type-in: npm install then wait till it finishes installing all the modules required to run our Node.js Web Application. +4. Once the installation is completed, type in the following command to run our Web Application: npm start Make sure to keep the CLI opened. +5. Now go to [http://127.0.0.1:3100/](http://127.0.0.1:3100/) using your favorite browser. + +## Contribution guidelines ++ ALWAYS start a new branch before making changes to the codes ++ Pull requests directly to the master branch will be ignored! ++ Use a git client, preferably Source Tree or you can use git commands from your terminal, your choice! ++ Many smaller commits are better than one large commit. Edit your file(s), if the edit does not break the code with things like syntax errors, commit. It is easier to reconcile many smaller commits than one large commit. ++ When your feature or bug fix is ready, perform a pull request and notify carl.fontanos@gmail.com that your code is ready for review on Github. + +## Author +### Carl Victor C. Fontanos ++ Website: [carlofontanos.com](http://www.carlofontanos.com) ++ Linkedin: [ph.linkedin.com/in/carlfontanos](http://ph.linkedin.com/in/carlfontanos) ++ Facebook: [facebook.com/carlo.fontanos](http://facebook.com/carlo.fontanos) ++ Twitter: [twitter.com/carlofontanos](http://twitter.com/carlofontanos) ++ Google+: [plus.google.com/u/0/107219338853998242780/about](https://plus.google.com/u/0/107219338853998242780/about) ++ GitHub: [github.com/carlo-fontanos](https://github.com/carlo-fontanos) \ No newline at end of file diff --git a/apis/app.js b/apis/app.js new file mode 100644 index 00000000..ea8809f3 --- /dev/null +++ b/apis/app.js @@ -0,0 +1,139 @@ +var express = require('express'); +var session = require('express-session'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); +var passport = require('passport'); +var LocalStrategy = require('passport-local').Strategy; +var flash = require('connect-flash'); +var md5 = require('crypto-md5'); + +/* MongoDB connection */ +var mongo = require('mongodb'); +var monk = require('monk'); +var db = monk('localhost:27017/nodetest1'); // 27017 is the default port for our MongoDB instance. +var users = db.get('users'); + +var routes = require('./routes/index'); + +var app = express(); + +/* Setup socket.io and express to run on same port (3100) */ +var server = require('http').Server(app); +var io = require('socket.io')(server); + +server.listen(3100); + +/* Realtime trigger */ +io.sockets.on('connection', function (socket) { + socket.on('send', function (data) { + io.sockets.emit('message', data); + }); +}); + +/* Define some globals that will be made accessible all through out the application */ +global.root_dir = path.resolve(__dirname); +global.uploads_dir = root_dir + '/public/images/uploads/'; + +/* view engine setup */ +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +/* Make the response uncompressed */ +app.locals.pretty = true; + +/* uncomment after placing your favicon in /public */ +/* app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); */ +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); +app.use(flash()); +app.use(session({ + secret: 'secret cat', + resave: true, + saveUninitialized: true +})); +app.use(passport.initialize()); +app.use(passport.session()); + +passport.use(new LocalStrategy({ + /* Define custom fields for passport */ + usernameField : 'email', + passwordField : 'password' + }, + function(email, password, done) { + /* validate email and password */ + users.findOne({email: email}, function(err, user) { + if (err) { return done(err); } + if (!user) { + return done(null, false, {message: 'Incorrect username.'}); + } + if (user.password != md5(password)) { + return done(null, false, {message: 'Incorrect password.'}); + } + /* if everything is correct, let's pass our user object to the passport.serializeUser */ + return done(null, user); + }); + } +)); + +passport.serializeUser(function(user, done) { + /* Attach to the session as req.session.passport.user = { email: 'test@test.com' } */ + /* The email key will be later used in our passport.deserializeUser function */ + done(null, user.email); +}); + +passport.deserializeUser(function(email, done) { + users.findOne({email: email}, function(err, user) { + /* The fetched "user" object will be attached to the request object as req.user */ + done(err, user); + }); +}); + + +app.use(function(req, res, next){ + req.db = db; /* Make our db accessible to our router */ + const { latitude, longitude } = req.user; + res.location.user = { latitude, longitude }; /* Make our location object accessible in all our templates. */ + next(); +}); + +app.use('/', routes); + +/* catch 404 and forward to error handler */ +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +/* error handlers */ + +/* development error handler */ +/* will print stacktrace */ +if (app.get('env') === 'development') { + app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + }); +} + +/* production error handler */ +/* no stacktraces leaked to user */ +app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +}); + + +module.exports = app; diff --git a/apis/bin/www b/apis/bin/www new file mode 100644 index 00000000..9e87e211 --- /dev/null +++ b/apis/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('nodetest1:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/apis/package.json b/apis/package.json new file mode 100644 index 00000000..016eefce --- /dev/null +++ b/apis/package.json @@ -0,0 +1,27 @@ +{ + "name": "nodetest1", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "async": "^2.0.1", + "body-parser": "~1.12.4", + "connect-flash": "^0.1.1", + "cookie-parser": "~1.3.5", + "crypto-md5": "^1.0.0", + "debug": "~2.2.0", + "express": "~4.12.4", + "express-session": "^1.14.1", + "jade": "~1.9.2", + "mongodb": "^1.4.4", + "monk": "^1.0.1", + "morgan": "~1.5.3", + "multer": "^1.2.0", + "passport": "^0.3.2", + "passport-local": "^1.0.0", + "serve-favicon": "~2.2.1", + "socket.io": "^1.4.8" + } +} diff --git a/apis/public/images/loading.gif b/apis/public/images/loading.gif new file mode 100644 index 00000000..cef73226 Binary files /dev/null and b/apis/public/images/loading.gif differ diff --git a/apis/public/images/logo.png b/apis/public/images/logo.png new file mode 100644 index 00000000..ac39411b Binary files /dev/null and b/apis/public/images/logo.png differ diff --git a/apis/public/javascripts/app.js b/apis/public/javascripts/app.js new file mode 100644 index 00000000..62f9e736 --- /dev/null +++ b/apis/public/javascripts/app.js @@ -0,0 +1,575 @@ +/** + * App Class + * + * @author Carl Victor Fontanos + * @author_url www.carlofontanos.com + * + */ + +/** + * Setup a App namespace to prevent JS conflicts. + */ + +var socket = io.connect('http://localhost:3100'); +var app = { + + + Posts: function() { + + /** + * This method contains the list of functions that needs to be loaded + * when the "Posts" object is instantiated. + * + */ + this.init = function() { + // this.loaded_posts_pagination(); + this.get_all_items_pagination(); + this.get_user_items_pagination(); + this.add_post(); + this.update_post(); + this.delete_post(); + this.unset_image(); + this.set_featured_image(); + this.set_imageviewer(); + } + + /** + * Load user items pagination. + */ + this.get_user_items_pagination = function() { + + _this = this; + + /* Check if our hidden form input is not empty, meaning it's not the first time viewing the page. */ + if($('form.post-list input').val()){ + /* Submit hidden form input value to load previous page number */ + data = JSON.parse($('form.post-list input').val()); + _this.ajax_get_user_items_pagination(data.page, data.th_name, data.th_sort); + } else { + /* Load first page */ + _this.ajax_get_user_items_pagination(1, 'name', 'ASC'); + } + + var th_active = $('.table-post-list th.active'); + var th_name = $(th_active).attr('id'); + var th_sort = $(th_active).hasClass('DESC') ? 'DESC': 'ASC'; + + /* Search */ + $('body').on('click', '.post_search_submit', function(){ + _this.ajax_get_user_items_pagination(1, th_name, th_sort); + }); + /* Search when Enter Key is triggered */ + $(".post_search_text").keyup(function (e) { + if (e.keyCode == 13) { + _this.ajax_get_user_items_pagination(1, th_name, th_sort); + } + }); + + /* Pagination Clicks */ + $('body').on('click', '.pagination-nav li.active', function(){ + var page = $(this).attr('p'); + var current_th_active = $('.table-post-list th.active'); + var current_sort = $(current_th_active).hasClass('DESC') ? 'DESC': 'ASC'; + var current_name = $(current_th_active).attr('id'); + _this.ajax_get_user_items_pagination(page, current_name, current_sort); + }); + + /* Sorting Clicks */ + $('body').on('click', '.table-post-list th', function(e) { + e.preventDefault(); + var th_name = $(this).attr('id'); + + if(th_name){ + /* Remove all TH tags with an "active" class */ + if($('.table-post-list th').removeClass('active')) { + /* Set "active" class to the clicked TH tag */ + $(this).addClass('active'); + } + if(!$(this).hasClass('DESC')){ + _this.ajax_get_user_items_pagination(1, th_name, 'DESC'); + $(this).addClass('DESC'); + } else { + _this.ajax_get_user_items_pagination(1, th_name, 'ASC'); + $(this).removeClass('DESC'); + } + } + }); + } + + /** + * AJAX user items pagination. + */ + this.ajax_get_user_items_pagination = function(page, th_name, th_sort){ + + if($(".pagination-container").length > 0 && $(".products-view-user").length > 0){ + $(".pagination-container").html(''); + + var post_data = { + page: page, + search: $('.post_search_text').val(), + th_name: th_name, + th_sort: th_sort, + max: $('.post_max').val(), + }; + + $('form.post-list input').val(JSON.stringify(post_data)); + + var data = { + action: "demo_load_my_posts", + data: JSON.parse($('form.post-list input').val()) + }; + + $.ajax({ + url: '/user/products/view', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(data), + success: function (response) { + + if($(".pagination-container").html(response.content)){ + $('.pagination-nav').html(response.navigation); + $('.table-post-list th').each(function() { + /* Append the button indicator */ + $(this).find('span.glyphicon').remove(); + if($(this).hasClass('active')){ + if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){ + $(this).append(' '); + } else { + $(this).append(' '); + } + } + }); + } + } + }); + } + } + + /** + * Load front-end items pagination. + */ + this.get_all_items_pagination = function() { + + _this = this; + + /* Check if our hidden form input is not empty, meaning it's not the first time viewing the page. */ + if($('form.post-list input').val()){ + /* Submit hidden form input value to load previous page number */ + data = JSON.parse($('form.post-list input').val()); + _this.ajax_get_all_items_pagination(data.page, data.name, data.sort); + } else { + /* Load first page */ + _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); + } + + /* Search */ + $('body').on('click', '.post_search_submit', function(){ + _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); + }); + /* Search when Enter Key is triggered */ + $(".post_search_text").keyup(function (e) { + if (e.keyCode == 13) { + _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); + } + }); + + /* Pagination Clicks */ + $('body').on('click', '.pagination-nav li.active', function(){ + var page = $(this).attr('p'); + _this.ajax_get_all_items_pagination(page, $('.post_name').val(), $('.post_sort').val()); + }); + } + + /** + * AJAX front-end items pagination. + */ + this.ajax_get_all_items_pagination = function(page, order_by_name, order_by_sort){ + + if($(".pagination-container").length > 0 && $('.products-view-all').length > 0 ){ + $(".pagination-container").html(''); + + var post_data = { + page: page, + search: $('.post_search_text').val(), + name: order_by_name, + sort: order_by_sort, + max: $('.post_max').val(), + }; + + $('form.post-list input').val(JSON.stringify(post_data)); + + var data = { + action: 'get-all-products', + data: JSON.parse($('form.post-list input').val()) + }; + + $.ajax({ + url: 'products/view-front', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(data), + success: function (response) { + + if($(".pagination-container").html(response.content)){ + $('.pagination-nav').html(response.navigation); + $('.table-post-list th').each(function() { + /* Append the button indicator */ + $(this).find('span.glyphicon').remove(); + if($(this).hasClass('active')){ + if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){ + $(this).append(' '); + } else { + $(this).append(' '); + } + } + }); + } + } + }); + } + } + + /** + * Submit updated data via ajax using jquery form plugin + */ + this.update_post = function(){ + $('.update-product').ajaxForm({ + beforeSerialize: function() { + update_ckeditor_instances(); + wave_box('on'); + }, + success: function(response, textStatus, xhr, form) { + if(response.status == 0){ + if($.isArray(response.errors)){ + $.each(response.errors, function (key, error_nessage) { + Lobibox.notify('error', {msg: error_nessage, size: 'mini', sound: false}); + }); + } + } + if(response.status == 1){ + if(response.message){ + Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); + } + + socket.emit('send', { message: $('.update-product').serializeObject() } ); + } + if(response.images){ + $('.image-input').val(''); + $('.no-item-images').remove(); + $.each(response.images, function (index, image) { + $('.images-section').append( + '
' + + '' + + '' + + '' + + '
' + ); + }); + } + wave_box('off'); + } + }); + } + + /** + * Submit new product data via ajax using jquery form plugin + */ + this.add_post = function(){ + $('.create-product').ajaxForm({ + beforeSubmit: function(arr, $form, options) { + var proceed = true; + + $('input.required').each(function(index) { + if($(this).val() == ''){ + Lobibox.notify('error', {msg: 'Please fill-up the required fields', size: 'mini', sound: false}); + proceed = false; + return false; + } + }); + + return proceed; + }, + beforeSerialize: function() { + update_ckeditor_instances(); + }, + success: function(response, textStatus, xhr, form) { + if(response == 0){ + Lobibox.notify('error', {msg: 'Failed to create the product, please try again', size: 'mini', sound: false}); + } else { + window.location.href = '/user/products/edit/' + response + '?status=created'; + } + } + }); + + if(get_url_value('status') == 'created'){ + Lobibox.notify('success', {msg: 'Item successfully created, you may continue editing this product.', size: 'mini', sound: false}); + } + } + + /** + * Handles the deletion of a single post + */ + this.delete_post = function(){ + $('body').on('click', '.delete-product', function(e) { + e.preventDefault(); + + var item = $(this); + var data = { + item_id: item.attr('item_id') + } + + $.ajax({ + url: '/user/products/delete', + type: 'POST', + data: data, + success: function (response) { + if(response == 0){ + Lobibox.notify('error', {msg: 'Delete failed, please try again', size: 'mini', sound: false}); + } else if(response == 1){ + item.parents('tr').css('background', '#add9ff').fadeOut('fast'); + Lobibox.notify('success', {msg: 'Deleted Successfully', size: 'mini', sound: false}); + } + } + }); + }); + + } + + /** + * Sends an AJAX request to delete the image. + */ + this.unset_image = function() { + $('body').on('click', '.unset-image', function(e) { + e.preventDefault(); + wave_box('on'); + + var parent_element = $(this).parent(); + + $.ajax({ + url: '/user/products/image/unset', + type: 'POST', + data: { + 'action': 'unset-image', + 'item_id': $('.item-edit').attr('id').split('-')[1], + 'image': this.id.split('-')[1] + }, + success: function (response) { + if(response.status == 1){ + parent_element.fadeOut(); + Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); + } else { + Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); + } + wave_box('off'); + } + }); + }); + } + + /** + * Sends an AJAX request to set the image as featured. + */ + this.set_featured_image = function() { + $('body').on('click', '.set-featured-image', function(e) { + e.preventDefault(); + wave_box('on'); + + var _this = this; + var image_featured_id = this.id.split('-')[1]; + + if( $(this).hasClass('glyphicon-star') ){ + Lobibox.notify('error', {msg: 'The image you clicked is already the featured image.', size: 'mini', sound: false}); + wave_box('off'); + + } else { + $.ajax({ + url: '/user/products/image/set-featured', + type: 'POST', + data: { + 'action': 'set-featured-image', + 'item_id': $('.item-edit').attr('id').split('-')[1], + 'image': image_featured_id + }, + datatype: 'JSON', + success: function (response) { + if(response.status == 1){ + if($('.images-section').find('span.glyphicon-star').switchClass('glyphicon-star', 'glyphicon-star-empty').removeAttr('style')){ + $(_this).switchClass('glyphicon-star-empty', 'glyphicon-star').css('color', '#E4C317'); + Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); + } + } else { + Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); + } + + socket.emit('send', { message: { featured: image_featured_id, id: $('.item-edit').attr('id').split('-')[1] } } ); + + wave_box('off'); + } + }); + } + }); + } + + /** + * Load ImageViewer plugin + */ + this.get_imageviewer_image = function(images, curImageIdx, viewer, curSpan){ + var imgObj = images[curImageIdx - 1]; + + viewer.load(imgObj.small, imgObj.big); + curSpan.html(curImageIdx); + } + + /** + * Initialize imageviewer plugin + */ + this.set_imageviewer = function() { + + _this = this; + + if($('input.item-images-json').length){ + var images = JSON.parse($('input.item-images-json').val()); + var curImageIdx = 1, + total = images.length; + var wrapper = $('.imageviewer'), + curSpan = wrapper.find('.current'); + var viewer = ImageViewer(wrapper.find('.image-container')); + + /* display total count */ + wrapper.find('.total').html(total); + + wrapper.find('.next').click(function(){ + curImageIdx++; + if(curImageIdx > total) curImageIdx = 1; + _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); + }); + + wrapper.find('.prev').click(function(){ + curImageIdx--; + if(curImageIdx < 1) curImageIdx = total; + _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); + }); + + /* initially show image */ + _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); + } + } + }, + + User: function() { + this.init = function() { + this.create_account(); + this.update_account(); + } + + this.create_account = function(){ + $('.create-account').ajaxForm({ + beforeSerialize: function() { + wave_box('on'); + }, + success: function(response, textStatus, xhr, form) { + if(response.status == 0){ + Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); + } + + if(response.status == 1){ + window.location.href = '/user/account'; + } + + wave_box('off'); + } + }); + } + + this.update_account = function(){ + $('.update-account').ajaxForm({ + beforeSerialize: function() { + update_ckeditor_instances(); + wave_box('on'); + }, + success: function(response, textStatus, xhr, form) { + if(response.status == 0){ + Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); + } + + if(response.status == 1){ + Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); + } + + wave_box('off'); + } + }); + } + + }, + + /** + * Global + */ + Global: function () { + + /** + * This method contains the list of functions that needs to be loaded + * when the "Global" object is instantiated. + * + */ + this.init = function() { + this.set_ckeditor(); + this.set_datepicker(); + } + + /** + * Load CKEditor plugin + */ + this.set_ckeditor = function() { + if($('#ck-editor-area').length){ + load_ckeditor('ck-editor-area', 300); + } + } + + /** + * Load CKEditor plugin + */ + this.set_datepicker = function() { + if('.datepicker'){ + $('.datepicker').datetimepicker({ + format: 'YYYY-MM-DD HH:mm:ss' + }); + } + } + } +} + +/** + * When the document has been loaded... + * + */ +jQuery(document).ready( function () { + + global = new app.Global(); /* Instantiate the Global Class */ + global.init(); /* Load Global class methods */ + + posts = new app.Posts(); /* Instantiate the Posts Class */ + posts.init(); /* Load Posts class methods */ + + user = new app.User(); /* Instantiate the User Class */ + user.init(); /* Load User class methods */ + + /* Update item data via real time */ + socket.on('message', function(data) { + var data = data.message; + var item_id = '.item-' + data.id; + for (var key in data) { + if (data.hasOwnProperty(key)) { + if(key == 'featured'){ + $(item_id + ' .item-featured').attr('src', '/images/uploads/' + data[key]); + } else if(key == 'price') { + $(item_id + ' .item-price').html(parseFloat(data[key]).toFixed(2)); + } else { + $(item_id + ' .item-' + key).html(data[key]); + } + } + } + }); + +}); \ No newline at end of file diff --git a/apis/public/javascripts/global.js b/apis/public/javascripts/global.js new file mode 100644 index 00000000..aed50331 --- /dev/null +++ b/apis/public/javascripts/global.js @@ -0,0 +1,75 @@ +/** + * Helper function to get append the loading image to message container when submitting via AJAX + * + * @param textarea, height + */ +function load_ckeditor( textarea, height ) { + CKEDITOR.config.allowedContent = true; + CKEDITOR.replace( textarea, { + toolbar: null, + toolbarGroups: null, + height: height + }); +} + +/** + * Helper function to command CKEditor to update the instancnes before performing the AJAX call. + * This will populate the hidden textfields with the proper values coming from the CKEditor + * + */ +function update_ckeditor_instances() { + for ( instance in CKEDITOR.instances ) { + CKEDITOR.instances[instance].updateElement(); + } +} + +/** + * Provides a nice wave animation effect + * + */ +function wave_box_animate(){ + if( $('.wave-box-effect').length ){ + jQuery( ".wave-box-effect" ).css( "left", "0px" ); + jQuery( ".wave-box-effect" ).animate( { 'left':"99%" }, 1000, wave_box_animate ); + } +} + +function wave_box(option) { + if($('.wave-box-wrapper').length){ + if(option == 'on'){ + if($(".wave-box-wrapper .wave-box").html('
').show()){ + wave_box_animate(); + } + } else if(option == 'off') { + $(".wave-box-wrapper .wave-box").html('').fadeOut(); + } + } +} + +/* Used for getting the parameter of a URL */ +function get_url_value(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i=0;i").get(0).files !== undefined; +feature.formdata = window.FormData !== undefined; + +var hasProp = !!$.fn.prop; + +// attr2 uses prop when it can but checks the return type for +// an expected string. this accounts for the case where a form +// contains inputs with names like "action" or "method"; in those +// cases "prop" returns the element +$.fn.attr2 = function() { + if ( ! hasProp ) { + return this.attr.apply(this, arguments); + } + var val = this.prop.apply(this, arguments); + if ( ( val && val.jquery ) || typeof val === 'string' ) { + return val; + } + return this.attr.apply(this, arguments); +}; + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + /*jshint scripturl:true */ + + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + var method, action, url, $form = this; + + if (typeof options == 'function') { + options = { success: options }; + } + else if ( options === undefined ) { + options = {}; + } + + method = options.type || this.attr2('method'); + action = options.url || this.attr2('action'); + + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + + options = $.extend(true, { + url: url, + success: $.ajaxSettings.success, + type: method || $.ajaxSettings.type, + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var traditional = options.traditional; + if ( traditional === undefined ) { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements); + if (options.data) { + options.extraData = options.data; + qx = $.param(options.data, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a, traditional); + if (qx) { + q = ( q ? (q + '&' + qx) : qx ); + } + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + if (options.resetForm) { + callbacks.push(function() { $form.resetForm(); }); + } + if (options.clearForm) { + callbacks.push(function() { $form.clearForm(options.includeHidden); }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + var fn = options.replaceTarget ? 'replaceWith' : 'html'; + $(options.target)[fn](data).each(oldSuccess, arguments); + }); + } + else if (options.success) { + callbacks.push(options.success); + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this ; // jQuery 1.4+ supports scope context + for (var i=0, max=callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + if (options.error) { + var oldError = options.error; + options.error = function(xhr, status, error) { + var context = options.context || this; + oldError.apply(context, [xhr, status, error, $form]); + }; + } + + if (options.complete) { + var oldComplete = options.complete; + options.complete = function(xhr, status) { + var context = options.context || this; + oldComplete.apply(context, [xhr, status, $form]); + }; + } + + // are there files to upload? + + // [value] (issue #113), also see comment: + // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219 + var fileInputs = $('input[type=file]:enabled', this).filter(function() { return $(this).val() !== ''; }); + + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + var fileAPI = feature.fileapi && feature.formdata; + log("fileAPI :" + fileAPI); + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + + var jqxhr; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + jqxhr = fileUploadIframe(a); + }); + } + else { + jqxhr = fileUploadIframe(a); + } + } + else if ((hasFileInputs || multipart) && fileAPI) { + jqxhr = fileUploadXhr(a); + } + else { + jqxhr = $.ajax(options); + } + + $form.removeData('jqxhr').data('jqxhr', jqxhr); + + // clear element array + for (var k=0; k < elements.length; k++) { + elements[k] = null; + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + // utility fn for deep serialization + function deepSerialize(extraData){ + var serialized = $.param(extraData, options.traditional).split('&'); + var len = serialized.length; + var result = []; + var i, part; + for (i=0; i < len; i++) { + // #252; undo param space replacement + serialized[i] = serialized[i].replace(/\+/g,' '); + part = serialized[i].split('='); + // #278; use array instead of object storage, favoring array serializations + result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]); + } + return result; + } + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i=0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + var serializedData = deepSerialize(options.extraData); + for (i=0; i < serializedData.length; i++) { + if (serializedData[i]) { + formdata.append(serializedData[i][0], serializedData[i][1]); + } + } + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType: false, + processData: false, + cache: false, + type: method || 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = $.ajaxSettings.xhr(); + if (xhr.upload) { + xhr.upload.addEventListener('progress', function(event) { + var percent = 0; + var position = event.loaded || event.position; /*event.position is deprecated*/ + var total = event.total; + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + options.uploadProgress(event, position, total, percent); + }, false); + } + return xhr; + }; + } + + s.data = null; + var beforeSend = s.beforeSend; + s.beforeSend = function(xhr, o) { + //Send FormData() provided by user + if (options.formData) { + o.data = options.formData; + } + else { + o.data = formdata; + } + if(beforeSend) { + beforeSend.call(this, xhr, o); + } + }; + return $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var deferred = $.Deferred(); + + // #341 + deferred.abort = function(status) { + xhr.abort(status); + }; + + if (a) { + // ensure that every serialized input is still enabled + for (i=0; i < elements.length; i++) { + el = $(elements[i]); + if ( hasProp ) { + el.prop('disabled', false); + } + else { + el.removeAttr('disabled'); + } + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + (new Date().getTime()); + if (s.iframeTarget) { + $io = $(s.iframeTarget); + n = $io.attr2('name'); + if (!n) { + $io.attr2('name', id); + } + else { + id = n; + } + } + else { + $io = $('