diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa6d91cd5..a8d7931a12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +### Features + +- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384)) + - We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others. + To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized). + For customization options, please check the [User Feedback documentation](https://docs.sentry.io/platforms/android/user-feedback/configuration/). + ```java + import io.sentry.android.core.SentryUserFeedbackDialog; + + new SentryUserFeedbackDialog(context).show(); + ``` + ```kotlin + import io.sentry.android.core.SentryUserFeedbackDialog + + SentryUserFeedbackDialog(context).show() + ``` + ### Fixes - Send UI Profiling app start chunk when it finishes ([#4423](https://github.com/getsentry/sentry-java/pull/4423)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index afb6b6a175..6e11818690 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -385,6 +385,15 @@ public final class io/sentry/android/core/SentryPerformanceProvider { public fun shutdown ()V } +public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog { + public fun (Landroid/content/Context;)V + public fun (Landroid/content/Context;I)V + public fun (Landroid/content/Context;ZLandroid/content/DialogInterface$OnCancelListener;)V + public fun setCancelable (Z)V + public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V + public fun show ()V +} + public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerformanceContinuousCollector, io/sentry/android/core/internal/util/SentryFrameMetricsCollector$FrameMetricsCollectorListener { protected final field lock Lio/sentry/util/AutoClosableReentrantLock; public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V diff --git a/sentry-android-core/src/main/AndroidManifest.xml b/sentry-android-core/src/main/AndroidManifest.xml index a304ee075b..6020a72db1 100644 --- a/sentry-android-core/src/main/AndroidManifest.xml +++ b/sentry-android-core/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + { + // Gather fields and trim them + final @NotNull String name = edtName.getText().toString().trim(); + final @NotNull String email = edtEmail.getText().toString().trim(); + final @NotNull String message = edtMessage.getText().toString().trim(); + + // If a required field is missing, shows the error label + if (name.isEmpty() && feedbackOptions.isNameRequired()) { + edtName.setError(lblName.getText()); + return; + } + + if (email.isEmpty() && feedbackOptions.isEmailRequired()) { + edtEmail.setError(lblEmail.getText()); + return; + } + + if (message.isEmpty()) { + edtMessage.setError(lblMessage.getText()); + return; + } + + // Create the feedback object + final @NotNull Feedback feedback = new Feedback(message); + feedback.setName(name); + feedback.setContactEmail(email); + if (currentReplayId != null) { + feedback.setReplayId(currentReplayId); + } + + // Capture the feedback. If the ID is empty, it means that the feedback was not sent + final @NotNull SentryId id = Sentry.captureFeedback(feedback); + if (!id.equals(SentryId.EMPTY_ID)) { + Toast.makeText( + getContext(), feedbackOptions.getSuccessMessageText(), Toast.LENGTH_SHORT) + .show(); + final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitSuccess = + feedbackOptions.getOnSubmitSuccess(); + if (onSubmitSuccess != null) { + onSubmitSuccess.call(feedback); + } + } else { + final @Nullable SentryFeedbackOptions.SentryFeedbackCallback onSubmitError = + feedbackOptions.getOnSubmitError(); + if (onSubmitError != null) { + onSubmitError.call(feedback); + } + } + cancel(); + }); + + btnCancel.setText(feedbackOptions.getCancelButtonLabel()); + btnCancel.setOnClickListener(v -> cancel()); + setOnDismissListener(delegate); + } + + @Override + public void setOnDismissListener(final @Nullable OnDismissListener listener) { + delegate = listener; + // If the user set a custom onDismissListener, we ensure it doesn't override the onFormClose + final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions(); + final @Nullable Runnable onFormClose = options.getFeedbackOptions().getOnFormClose(); + if (onFormClose != null) { + super.setOnDismissListener( + dialog -> { + onFormClose.run(); + currentReplayId = null; + if (delegate != null) { + delegate.onDismiss(dialog); + } + }); + } else { + super.setOnDismissListener(delegate); + } + } + + @Override + protected void onStart() { + super.onStart(); + final @NotNull SentryOptions options = Sentry.getCurrentScopes().getOptions(); + final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions(); + final @Nullable Runnable onFormOpen = feedbackOptions.getOnFormOpen(); + if (onFormOpen != null) { + onFormOpen.run(); + } + options.getReplayController().captureReplay(false); + currentReplayId = options.getReplayController().getReplayId(); + } + + @Override + public void show() { + // If Sentry is disabled, don't show the dialog, but log a warning + final @NotNull IScopes scopes = Sentry.getCurrentScopes(); + final @NotNull SentryOptions options = scopes.getOptions(); + if (!scopes.isEnabled() || !options.isEnabled()) { + options + .getLogger() + .log(SentryLevel.WARNING, "Sentry is disabled. Feedback dialog won't be shown."); + return; + } + // Otherwise, show the dialog + super.show(); + } +} diff --git a/sentry-android-core/src/main/res/drawable-hdpi/sentry_logo_dark.webp b/sentry-android-core/src/main/res/drawable-hdpi/sentry_logo_dark.webp new file mode 100644 index 0000000000..107533bbf2 Binary files /dev/null and b/sentry-android-core/src/main/res/drawable-hdpi/sentry_logo_dark.webp differ diff --git a/sentry-android-core/src/main/res/drawable-mdpi/sentry_logo_dark.webp b/sentry-android-core/src/main/res/drawable-mdpi/sentry_logo_dark.webp new file mode 100644 index 0000000000..6237c1c7e8 Binary files /dev/null and b/sentry-android-core/src/main/res/drawable-mdpi/sentry_logo_dark.webp differ diff --git a/sentry-android-core/src/main/res/drawable-xhdpi/sentry_logo_dark.webp b/sentry-android-core/src/main/res/drawable-xhdpi/sentry_logo_dark.webp new file mode 100644 index 0000000000..d072a19a43 Binary files /dev/null and b/sentry-android-core/src/main/res/drawable-xhdpi/sentry_logo_dark.webp differ diff --git a/sentry-android-core/src/main/res/drawable-xxhdpi/sentry_logo_dark.webp b/sentry-android-core/src/main/res/drawable-xxhdpi/sentry_logo_dark.webp new file mode 100644 index 0000000000..7dceaec7fa Binary files /dev/null and b/sentry-android-core/src/main/res/drawable-xxhdpi/sentry_logo_dark.webp differ diff --git a/sentry-android-core/src/main/res/drawable-xxxhdpi/sentry_logo_dark.webp b/sentry-android-core/src/main/res/drawable-xxxhdpi/sentry_logo_dark.webp new file mode 100644 index 0000000000..0e61b7ebce Binary files /dev/null and b/sentry-android-core/src/main/res/drawable-xxxhdpi/sentry_logo_dark.webp differ diff --git a/sentry-android-core/src/main/res/drawable/edit_text_border.xml b/sentry-android-core/src/main/res/drawable/edit_text_border.xml new file mode 100644 index 0000000000..5615e31857 --- /dev/null +++ b/sentry-android-core/src/main/res/drawable/edit_text_border.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/sentry-android-core/src/main/res/layout/sentry_dialog_user_feedback.xml b/sentry-android-core/src/main/res/layout/sentry_dialog_user_feedback.xml new file mode 100644 index 0000000000..ca54898eb8 --- /dev/null +++ b/sentry-android-core/src/main/res/layout/sentry_dialog_user_feedback.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + +