Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor API to use strongly typed generics and push/pop style model #336

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion feedback/example/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>12.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion feedback/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
17 changes: 10 additions & 7 deletions feedback/example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -164,7 +164,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down Expand Up @@ -214,14 +214,14 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
);
Expand All @@ -232,10 +232,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
Expand All @@ -246,6 +248,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -355,7 +358,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -440,7 +443,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -489,7 +492,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 1 addition & 1 deletion feedback/example/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import UIKit
import Flutter

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
2 changes: 2 additions & 0 deletions feedback/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
46 changes: 26 additions & 20 deletions feedback/example/lib/custom_feedback.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'dart:typed_data';

import 'package:feedback/feedback.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

/// A data type holding user feedback consisting of a feedback type, free from
Expand Down Expand Up @@ -53,11 +56,11 @@ class CustomFeedbackForm extends StatefulWidget {
const CustomFeedbackForm({
super.key,
required this.onSubmit,
required this.scrollController,
required this.formController,
});

final OnSubmit onSubmit;
final ScrollController? scrollController;
final FeedbackFormController<UserFeedback> formController;

@override
State<CustomFeedbackForm> createState() => _CustomFeedbackFormState();
Expand All @@ -73,13 +76,11 @@ class _CustomFeedbackFormState extends State<CustomFeedbackForm> {
Expanded(
child: Stack(
children: [
if (widget.scrollController != null)
const FeedbackSheetDragHandle(),
if (widget.formController.scrollController != null) const FeedbackSheetDragHandle(),
ListView(
controller: widget.scrollController,
controller: widget.formController.scrollController,
// Pad the top by 20 to match the corner radius if drag enabled.
padding: EdgeInsets.fromLTRB(
16, widget.scrollController != null ? 20 : 16, 16, 0),
padding: EdgeInsets.fromLTRB(16, widget.formController.scrollController != null ? 20 : 16, 16, 0),
children: [
const Text('What kind of feedback do you want to give?'),
Row(
Expand All @@ -99,16 +100,11 @@ class _CustomFeedbackFormState extends State<CustomFeedbackForm> {
.map(
(type) => DropdownMenuItem<FeedbackType>(
value: type,
child: Text(type
.toString()
.split('.')
.last
.replaceAll('_', ' ')),
child: Text(type.toString().split('.').last.replaceAll('_', ' ')),
),
)
.toList(),
onChanged: (feedbackType) => setState(() =>
_customFeedback.feedbackType = feedbackType),
onChanged: (feedbackType) => setState(() => _customFeedback.feedbackType = feedbackType),
),
ElevatedButton(
child: const Text('Open Dialog #2'),
Expand All @@ -132,8 +128,7 @@ class _CustomFeedbackFormState extends State<CustomFeedbackForm> {
const SizedBox(height: 16),
const Text('What is your feedback?'),
TextField(
onChanged: (newFeedback) =>
_customFeedback.feedbackText = newFeedback,
onChanged: (newFeedback) => _customFeedback.feedbackText = newFeedback,
),
const SizedBox(height: 16),
const Text('How does this make you feel?'),
Expand All @@ -149,10 +144,21 @@ class _CustomFeedbackFormState extends State<CustomFeedbackForm> {
TextButton(
// disable this button until the user has specified a feedback type
onPressed: _customFeedback.feedbackType != null
? () => widget.onSubmit(
_customFeedback.feedbackText ?? '',
extras: _customFeedback.toMap(),
)
? () async {
final Uint8List screenshot = await widget.formController.takeScreenshot(context);
if (!context.mounted) {
// User popped the page while screenshotting, abort.
return;
}
await widget.onSubmit(
context,
UserFeedback(
text: _customFeedback.feedbackText ?? '',
screenshot: screenshot,
extra: _customFeedback.toMap(),
),
);
}
: null,
child: const Text('submit'),
),
Expand Down
108 changes: 100 additions & 8 deletions feedback/example/lib/feedback_functions.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,62 @@
// ignore_for_file: avoid_print

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:feedback/feedback.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';

/// Map from a string route to the underlying feedback route and it's corresponding `onSubmit`.
/// This is just here to support the legacy `simpleFeedback` API.
Future<void> onSubmitWithStringRoute(
BuildContext context,
String? route,
UserFeedback feedback,
) {
return switch (route) {
'alert_dialog' => onSubmitAlertDialog(context, feedback),
'email' => onSubmitEmail(context, feedback),
'platform_sharing' => onSubmitPlatformSharing(context, feedback),
_ => throw UnsupportedError('Unsupported route: `$route`'),
};
}

Future<void> onSubmitEmail(BuildContext context, UserFeedback feedback) async {
// draft an email and send to developer
final screenshotFilePath = await writeImageToStorage(feedback.screenshot);

final Email email = Email(
body: feedback.text,
subject: 'App Feedback',
recipients: ['[email protected]'],
attachmentPaths: [screenshotFilePath],
isHTML: false,
);
await FlutterEmailSender.send(email);
}

Future<void> onSubmitPlatformSharing(BuildContext context, UserFeedback feedback) async {
final screenshotFilePath = await writeImageToStorage(feedback.screenshot);

// ignore: deprecated_member_use
await Share.shareFiles(
[screenshotFilePath],
text: feedback.text,
);
}

Future<String> writeImageToStorage(Uint8List feedbackScreenshot) async {
final Directory output = await getTemporaryDirectory();
final String screenshotFilePath = '${output.path}/feedback.png';
final File screenshotFile = File(screenshotFilePath);
await screenshotFile.writeAsBytes(feedbackScreenshot);
return screenshotFilePath;
}

/// Prints the given feedback to the console.
/// This is useful for debugging purposes.
Expand All @@ -17,14 +72,9 @@ void consoleFeedbackFunction(
}
}

/// Shows an [AlertDialog] with the given feedback.
/// This is useful for debugging purposes.
void alertFeedbackFunction(
BuildContext outerContext,
UserFeedback feedback,
) {
showDialog<void>(
context: outerContext,
Future<void> onSubmitAlertDialog(BuildContext context, UserFeedback feedback) async {
await showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(feedback.text),
Expand Down Expand Up @@ -54,3 +104,45 @@ void alertFeedbackFunction(
},
);
}

Future<void> createGitlabIssueFromFeedback(BuildContext context, UserFeedback feedback) async {
const projectId = 'your-gitlab-project-id';
const apiToken = 'your-gitlab-api-token';

final screenshotFilePath = await writeImageToStorage(feedback.screenshot);

// Upload screenshot
final uploadRequest = http.MultipartRequest(
'POST',
Uri.https(
'gitlab.com',
'/api/v4/projects/$projectId/uploads',
),
)
..files.add(await http.MultipartFile.fromPath(
'file',
screenshotFilePath,
))
..headers.putIfAbsent('PRIVATE-TOKEN', () => apiToken);
final uploadResponse = await uploadRequest.send();

final dynamic uploadResponseMap = jsonDecode(
await uploadResponse.stream.bytesToString(),
);

// Create issue
await http.post(
Uri.https(
'gitlab.com',
'/api/v4/projects/$projectId/issues',
<String, String>{
'title': feedback.text.padRight(80),
'description': '${feedback.text}\n'
"${uploadResponseMap["markdown"] ?? "Missing image!"}",
},
),
headers: {
'PRIVATE-TOKEN': apiToken,
},
);
}
Loading
Loading